---
# System prepended metadata

title: Desafio Regional Mud Runner Baja

---

Desafio Regional Mud Runner Baja
===
Este artigo apresenta e explica a metodologia usada na solução do desafio técnico de eletrônica, proposto pela SAE (Sociedade de Engenheiros Automotivos) para as equipes de baja competidoras na regional sudeste de 2022. Aqui se explica como os dados foram tratados e para que fins.

Utilizou-se a linguagem de script Python com a IDE Jupyter Notebook. As principais bibliotecas usadas foram Pandas, matplotlib e numpy. Solução desenvolvida e apresentada pela equipe de eletrônica da Mud Runner Baja:
Juliana Fortunato
Lucas Berton
Lucas Tocci
Rafael Henrique

Texto explicativo das metodologias usadas
---
### Informações sobre os dados utilizados
>Event: 2014 Targa 66<br>
Venue: Palm Beach International Raceway<br>
Date: 2014-02-21<br>
Driver: Participant 3<br>
Vehicle: 1965 Ferrari 250 LM Berlinetta GT<br>

| Informação | Dataset 0 | Dataset 1 | Dataset 2 | Dataset 3  |
| ---------- | --------- | --------- | --------- | --------- |
| Time (UTC) | 14:29:02  | 14:37:58  | 16:07:11  | 18:43:46  |
| Session | 01 | 01 | 02 | 03 |
| Outing | 01 | 02 | 01 | 01 |


Os datasets foram enumerados de 0 a 3, com base nessas informações. Os dados analisados estão de acordo com a tabela abaixo e podem ser baixados no link: [exhibits.stanford.edu/data/catalog/hd122pw0365](https:// "exhibits.stanford.edu/data/catalog/hd122pw0365")



| Arquivo | Nome |
| :-----: | :--- |
| 1 | 20140221_01_01_03_250lm.csv |
| 2 | 20140221_01_02_03_250lm.csv |
| 3 | 20140221_02_01_03_250lm.csv |
| 4 | 20140221_03_01_03_250lm.csv |
| 5 | channels.txt |


### Ponto de largada
O ponto de partida da pista foi arbitrado a partir das cordenadas geográficas coletadas, se baseando na pista Palm Beach International Raceway. Com isso, define-se o início e o final de qualquer volta. Como a pista para todos os datasets foi a mesma, utilizou-se da mesma regra para todos os datasets.

A coordenada foi definida após a análise do gráfico posição latitudinal x posição longitudinal de todos os datasets. Uma volta se inicia ou termina no primeiro instante (tempo) em que os dados correspondem às condições: 

Latitude aproximada para 4 casas decimais igual 26.9216 e longitude aproximada para 4 casas decimais menor que -80.3056. A demarcação azul no gráfico demonstra a posição das coordenadas que se encaixam nessas regras.

        Como foi feita a busca:
    tabela = tabela[round(tabela["latitude"], 4) == 26.9216]
    tabela = tabela[round(tabela["longitude"], 4) < -80.3056]
    
![GrafDS1_Largada_Arbitrada.png](https://i.imgur.com/GUQ0Zqv.png)
    
### Tempos de voltas
A partir da referência (26.9216, -80.3056), encontram-se os momentos em que o piloto passa pela linha de chegada. Nota-se que para cada conjunto de dados, o primeiro tempo representa o início da primeira volta e o último dado o fim da última volta. 

Os dados após o fim da última volta são referentes a volta do piloto para o box - que, nessas circunstâncias, não cruza mais a linha de chegada. O código para encontrar os timeStamps está na sessão "Encontrando os Timestamps".
    
    [t0, t1, t2, ... tn], onde:
    t0 é início das voltas, t1 é fim da primeira volta e início da segunda e tn é o fim da última volta
    
    timeStamps = {
    [45.62, 187.13],                                # Tempos dataset0
    [234.8, 351.13, 459.19, 562.5],                 # Tempos dataset1
    [38.09, 152.9, 254.93, 357.29, 456.11, 552.21], # Tempos dataset2
    [152.57, 264.46]                                # Tempos dataset3
    }
    
### Velocidade absoluta média, máxima, tempo das voltas e distância percorrida
O canal horizontalSpeed (velocidade horizontal) se refere a velocidade absoluta do carro nos eixos horizontais (comprimento (frente/trás) e largura (esquerda/direita)), excluindo o eixo vertical (cima/baixo, ou altura).

Formulou-se uma tabela apenas com os momentos de interesse para os cálculos das velocidades média e máxima. Considerou-se válidos apenas os dados que estão inseridos entre a primeira e a última volta, para cada dataset. A volta para o box e qualquer momento antes do piloto cruzar a largada serão desconsiderados.

Como se optou por iterar pela descrição das tabelas (funcionalidade da biblioteca pandas) para encontrar os dados, calculou-se também o tempo de cada volta. A diferença (em valores absolutos) entre um timeStamp e o próximo é o tempo que o piloto levou para concluir aquela volta. O código está na sessão "Velocidade absoluta média, máxima e tempo das voltas".

Por fim, com o tempo de cada volta e a velocidade média durante o percurso, calcula-se a distância percorrida em cada volta. Esse valor não representa exatamente o comprimento da pista - já que a pista é larga e o piloto pode fazer curvas abertas ou fechadas. 

O comprimento da pista, em função de regulamentos, é a média entre o comprimento das paredes internas e das paredes externas: simulando um piloto que sempre passa pelo centro da pista, nunca nos cantos internos e externos.<br>

| DatasetRef | NumVolta | TempoVolta |  VelMedia  | VelMax | DistPerc |
| ---------- | -------- | ---------- |  --------  | ------ | ------------- |
| Dataset 0  | Volta 1  |  141.510s  | 22.055 m/s | 33.96 m/s | 3121.01 m |
| Dataset 1  | Volta 2  |  116.330s  | 26.746 m/s | 53.64 m/s | 3111.31 m |
| Dataset 1  | Volta 3  |  108.060s  | 28.708 m/s | 55.67 m/s | 3102.22 m |
| Dataset 1  | Volta 4  |  103.310s  | 30.124 m/s | 56.62 m/s | 3112.06 m |
| Dataset 2  | Volta 5  |  114.810s  | 27.075 m/s | 52.31 m/s | 3108.47 m |
| Dataset 2  | Volta 6  |  102.030s  | 30.434 m/s | 56.57 m/s | 3105.14 m |
| Dataset 2  | Volta 7  |  102.360s  | 30.313 m/s | 58.86 m/s | 3102.83 m |
| Dataset 2  | Volta 8  |  98.820s  | 31.510 m/s | 60.13 m/s | 3113.79 m |
| Dataset 2  | Volta 9  |  96.100s  | 32.353 m/s | 60.55 m/s | 3109.10 m |
| Dataset 3  | Volta 10 |  111.890s  | 27.746 m/s | 55.3 m/s | 3104.47 m |


Como o piloto busca realizar curvas fechadas (para reduzir a distância percorrida e, consequentemente, o tempo da volta), é seguro dizer que a distância percorrida pelo piloto é, na maioria dos casos, menor que o comprimento da pista. Ao se comparar o maior valor encontrado (3121m) com o comprimento oficial da pista (3200m), encontra-se um erro aproximado de 2,5%.

## Código
### Bibliotecas

    import pandas as pd
    import matplotlib.pyplot as plt
    import numpy as np 
    import matplotlib.animation as animation
    from IPython.display import clear_output

    # Para salvar videos em ffmpeg é necessário baixar os plugins. Há pacotes instaláveis no Conda ou,
    # alternativamente, pode-se instalar por fora e adicionar ao Path do Windows.
    # Por fim, pode-se instalar em qualquer lugar e referir para o matplotlib onde encontrá-lo com:
    # plt.rcParams['animation.ffmpeg_path'] = r'C:\FFmpeg\bin\ffmpeg.exe'

### Preparativos
Aqui tratamos do setup do dataframe, lemos os arquivos para o programa e declaramos algumas variáveis. Os TimeStamps foram descobertos numa etapa posterior, mas retrospectivamente adicionados no preparativo.

    dtypes = {
        "time": float, "handwheelAngle": float, "throttle": float, "brake": float, "clutch": float, "engineSpeed": float,
        "PPS": float, "gpsTime": float, "latitude": float, "longitude": float, "altitude": float,  "vNorth": float, "vEast": float, 
        "vUp": float, "horizontalSpeed": float, "vxCG": float, "vyCG": float, "vzCG": float, "axCG": float, "ayCG": float,
        "azCG": float, "yawAngle": float, "pitchAngle": float, "rollAngle": float, "rollRate": float, "pitchRate": float,
        "yawRate": float, "sideSlip": float, "distance": float, "numSVsTracked": float, "gpsPosMode": float, "gpsVelMode": float,
        "gpsOrientMode": float, "HDOP": float, "PDOP": float, "posAccuracy_north": float, "posAccuracy_east": float, 
        "posAccuracy_down": float, "velAccuracy_north": float, "velAccuracy_east": float, "velAccuracy_down": float,
        "orientAccuracy_heading": float, "orientAccuracy_pitch": float, "orientAccuracy_roll": float, "chassisAccelFL": float,
        "chassisAccelFR": float, "chassisAccelRL": float, "chassisAccelRR": float, "deflectionFL": float, "deflectionFR": float,
        "deflectionRR": float, "deflectionRR": float, "wheelAccelFL": float, "wheelAccelFR": float, "wheelAccelRL": float,
        "wheelAccelRR": float,
    }

    names = ["time","handwheelAngle","throttle","brake","clutch","engineSpeed","PPS","gpsTime","latitude","longitude",
             "altitude","vNorth","vEast","vUp","horizontalSpeed","vxCG","vyCG","vzCG","axCG","ayCG","azCG","yawAngle",
             "pitchAngle","rollAngle","rollRate","pitchRate","yawRate","sideSlip","distance","numSVsTracked",
             "gpsPosMode","gpsVelMode","gpsOrientMode","HDOP","PDOP","posAccuracy_north","posAccuracy_east",
             "posAccuracy_down","velAccuracy_north","velAccuracy_east","velAccuracy_down","orientAccuracy_heading",
             "orientAccuracy_pitch","orientAccuracy_roll","chassisAccelFL","chassisAccelFR","chassisAccelRL",
             "chassisAccelRR","deflectionFL","deflectionFR","deflectionRL","deflectionRR","wheelAccelFL",
             "wheelAccelFR","wheelAccelRL","wheelAccelRR"]

    QTD_DATASETS = 4
    dsarray = [] # Array de datasets. dsarray[i] = dataseti.csv
    # ds1 = pd.read_csv("dataset1.csv", encoding ="ISO-8859-1", on_bad_lines='skip', dtype=dtypes, skiprows = 12, names = names1e4)

    for i in range(QTD_DATASETS):
        dsfilename = "dataset" + str(i) + ".csv"
        dsarray.append(pd.read_csv(dsfilename, encoding ="ISO-8859-1", on_bad_lines='skip',
                            dtype=dtypes, skiprows = 12, names = names)) 

    timeStamps = [
        [45.62, 187.13], # Tempos dataset0
        [234.8, 351.13, 459.19, 562.5], # Tempos dataset1
        [38.09, 152.9, 254.93, 357.29, 456.11, 552.21], #Tempos dataset2
        [152.57, 264.46] # Tempos dataset3
    ]
    timeStampsRound = [
        [46, 187], # Tempos dataset0
        [235, 351, 459, 563], # Tempos dataset1
        [38, 153, 255, 357, 456, 552], #Tempos dataset2
        [153, 264] # Tempos dataset3
    ]

### Definição de funções usadas

    def vAbsoluta(vNorth, vEast):
        return ((abs(vEast)**2 + abs(vNorth)**2)**(1/2))

    def computeCircuitNumber(timeStamp):
        if timeStamp in timeStampsRound[dataset_global]:
            global lapNumber
            lapNumber = lapNumber + 1
        if  lapNumber >= 0:
            if len(lapTimes) == lapNumber:
                lapTimes.append(1)
            else:
                lapTimes[lapNumber] += 1
        return lapNumber, lapTimes      

    def init():
            line.set_data([], [])
            return line,

    def animate(i):
            dft = dsf[(dsf.index == i)]
            x = dft['latitude'].values[0]
            y = dft['longitude'].values[0]
            x_data.append(x)
            y_data.append(y)
            lapNum, lapTimes = computeCircuitNumber(dft["time"].values[0])
            horizontalSpeed = dft['horizontalSpeed'].values[0]

            speedText.set_text("Velocidade: " + str(round(horizontalSpeed,2)) + " m/s")
            title.set_text("Tempo em segundos: " + str(dft["time"].values[0]) + "s")
            xlabel.set_text("Latitude: " + str(x))
            ylabel.set_text("Longitude: " + str(y))
            if lapNum < 0:
                text.set_text('Carro fora da pista')

            else:
                text.set_text('Volta ' + str(lapNum+1) + ': ' + str(lapTimes[lapNum]) + ' segundos')

            if len(x_data) > 1:
                 line.set_markevery(len(x_data)-1)
            line.set_data(x_data, y_data)

            return line,

### Vídeo da posição no tempo

    # Criando video dos gráficos, para todos os 4 datasets
    dataset_global = 0
    for i in range(QTD_DATASETS):
        dataset_global = i
        dsf = dsarray[i].groupby(np.arange(len(dsarray[i]))//1000).mean()
        dsf["time"] = dsf["time"].astype(int)

        plt.style.use('seaborn-pastel')

        fig = plt.figure()
        latMin = dsf['latitude'].min(False,True)
        latMax = dsf['latitude'].max()
        longMax = dsf['longitude'].max()
        longMin = dsf['longitude'].min()
        ax = plt.axes(xlim=(latMin, latMax), ylim=(longMin, longMax))
        line, = ax.plot([26.9238], [-80.30595], lw=3)
        line.set_marker('o')
        line.set_markeredgecolor('r')
        line.set_markerfacecolor('r')

        ax.set_yticklabels([])
        ax.set_xticklabels([])

        lapTimes = []
        lapNumber = -1

        text = plt.text(latMin, longMin, 'Carro fora da pista', fontsize=12)
        speedText = plt.text(latMin, longMin+ 0.0001, 'Velocidade: 0.0 m/s', fontsize=12)

        title = plt.title("Tempo em segundos: ")
        xlabel = plt.xlabel("Latitude: ")
        ylabel = plt.ylabel("Longitude: ")

        x_data = []
        y_data = []

        anim = animation.FuncAnimation(fig, animate, init_func=init,
                                       frames=len(dsf), interval=20, blit=True)

        Writer = animation.writers['ffmpeg']
        writer = Writer(fps=15, bitrate=1000)
        filename = "dataset" + str(i) + ".mp4"
        anim.save(filename, writer=writer)

### Gráficos Latitude x Longitude

    # Plotando e salvando os gráficos para cada dataset
    for i in range(QTD_DATASETS):
        ds = dsarray[i].plot(
            x = 'latitude',
            y = 'longitude',
            kind = 'scatter',
            color = 'purple',
            fontsize = 72,
            figsize = (60, 15),
        )
        fig = ds.get_figure()

        plt.grid(which='both')

        # latMin = dsarray[i]['latitude'].min(False,True)
        # latMax = dsarray[i]['latitude'].max()
        # longMax = dsarray[i]['longitude'].max()
        # longMin = dsarray[i]['longitude'].min()

        # Valores definidos para melhor visualização no gráfico
        xMin = 26.9175
        xMax = 26.9275
        yMin = -80.3075
        yMax = -80.3045

        # Limite dos eixos dos gráficos
        ds.set_xlim(xMin,xMax)
        ds.set_ylim(yMin,yMax )
        # ds.set_xlim(latMin, latMax)
        # ds.set_ylim(lonMin, lonMax)

        # Posição e passos das linhas de grade
        x_major_ticks = np.arange(26.918, 26.9275, 0.001000)
        x_minor_ticks = np.arange(26.9175, 26.9275, 0.000100)
        y_major_ticks = np.arange(-80.3075, -80.3045, 0.000500)
        y_minor_ticks = np.arange(-80.3075, -80.3045, 0.000100)

        ds.set_xlabel('Latitude', fontsize = 96, weight='bold')
        ds.set_ylabel('Longitude', fontsize = 96, weight='bold')

        ds.set_xticks(x_major_ticks)
        ds.set_xticks(x_minor_ticks, minor=True)
        ds.set_yticks(y_major_ticks)
        ds.set_yticks(y_minor_ticks, minor=True)

        ds.grid(which='minor', alpha=0.6, color='black')
        ds.grid(which='major', alpha=1.0, color='red')

        fig.savefig("grafico" + str(i) + ".png")
        ds.plot()

### Encontrando os timestamps

    # Encontrando os instantes que as voltas se iniciam e terminam
    # Lembrando que o último tempo indica a última volta e que, mesmo que o piloto
    # "conclua mais uma volta", na verdade, ele está voltando para o box e não cruza
    # a largada nesse percurso.
    dsets = []
    for ds in range(QTD_DATASETS):

        # Definindo tabela com as colunas tempo, latitude, longitude, vnorth e veast
        tabela = dsarray[ds].loc[:,["time", "latitude", "longitude", "vNorth", "vEast"]]

        # Excluindo valores desinteressantes
        tabela = tabela[tabela["latitude"].notnull()][tabela["longitude"].notnull()]
        tabela = tabela[((tabela["vNorth"] != 0) & (tabela["vEast"]) != 0)]

        # Selecionando apenas os momentos próximos da largada
        tabela = tabela[round(tabela["latitude"], 4) == 26.9216]
        tabela = tabela[round(tabela["longitude"], 4) < -80.3056]


        # tReal é o primeiro momento que buscamos.
        # tTest é tReal acrescido de um valor alto o bastante para prevenir
        # que a busca associe dois momentos (tReais) a uma mesma volta.
        tReal = 0.0
        tTest = tReal

        # Como a tabela só possui as linhas para as coordenadas buscadas,
        # É necessário apenas buscar os tempos que distam mais de 3 segundos
        voltas = []
        for i in range(len(tabela)):
            t = tabela.iat[i,0]
            if t  > tTest:
                tReal = t
                tTest = t + 3
                voltas.append(tReal)
        dsets.append(voltas)

    clear_output()
    print("------------------------------------")
    for i in range(QTD_DATASETS):
        print("Timestamps dataset{0}: {1}; {2} Voltas\n".format(i,dsets[i], len(dsets[i])-1))
    print("------------------------------------")

### Velocidade absoluta média, máxima e tempo das voltas

    timeStamps = [
        [45.62, 187.13], # Tempos dataset0
        [234.8, 351.13, 459.19, 562.5], # Tempos dataset1
        [38.09, 152.9, 254.93, 357.29, 456.11, 552.21], #Tempos dataset2
        [152.57, 264.46] # Tempos dataset3
    ]
    dsVoltas = []
    iMed = 1
    iMax = 7
    for i in range(QTD_DATASETS):
        voltas = []
        for j in range(len(timeStamps[i]) - 1):
            tabela = dsarray[i].loc[:,["time", "horizontalSpeed"]]
            
            tabela = tabela[tabela["time"] >= timeStamps[i][j]]
            tabela = tabela[tabela["time"] <= timeStamps[i][j+1]]
            
            tvolta = timeStamps[i][j+1] - timeStamps[i][j]
            vmed = tabela.describe().iat[1,1] # get[média, horizontalSpeed]
            vmax = tabela.describe().iat[7,1] # get[máx, horizontalSpeed]
            sperc = tvolta * vmed
            
            volta = [tvolta, vmed, vmax, sperc]
            voltas.append(volta)
        dsVoltas.append(voltas)
    
    print("| DatasetRef | NumVolta | TempoVolta |  VelMedia  | VelMax | DistânciaPerc |")
    print("| ---------- | -------- | ---------- |  --------  | ------ | ------------- |")
    for i in range(QTD_DATASETS):
        for voltas in range(len(dsVoltas[i])):
            t = dsVoltas[i][voltas][0]
            vmed = dsVoltas[i][voltas][1]
            vmax = dsVoltas[i][voltas][2]
            sperc = dsVoltas[i][voltas][3]
            print("| Dataset {0}  | Volta {1}  |  {2:03.3f}s  | {3:.3f} m/s |
            {4} m/s | {5:.2f} m |".format(i,voltas+1,t,vmed,vmax,sperc))
    # tabela.describe().iat[1,1] # get[média, horizontalSpeed]
    # tabela.describe().iat[7,1] # get[máx, horizontalSpeed]