diff --git a/README.md b/README.md index 3a6ff93..e335fe1 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,28 @@ # Sims-Data + Vehicle Dynamics Simulations and Data Analysis for Formula Slug written from the ground up in python. + + + +\# Blue Max Turn Speed Analysis + + + +This script analyzes vehicle turn speed against duration at various designated turn points on the Blue Max Track to figure out any statistical correlations/ calculations that could be derived from them and provide the driver with better feedback. + + + +Status: + +I have successfully created a working script for a single blue max track session file that runs the necessary data and outputs a graph depicting the project scope. + +However, I am stuck while trying to aggregate this code onto all the Blue Max sessions through the combined file that Nathaniel made. I'm running into primarily + +lap designation errors where the code isn't depicting all the laps involved in the file as valid data points and ignores most of the laps' data. After trying to debug + +I think it has something to do with the fact that I use static GPS points based on points on the track and when using the combined file which is not just a single continuous + +session, these gps points skew resulting in invalid data. If this is true, I don't know how to address the problem but in the meantime I was thinking of individually analyzing + +each session (as my code does run for them that way) and aggregating the data at the end. + diff --git a/turntimespeed.py b/turntimespeed.py new file mode 100644 index 0000000..c5d1b56 --- /dev/null +++ b/turntimespeed.py @@ -0,0 +1,150 @@ +## Problem right now is that the laps are not being recognized as valid, thus cant determine turn speed +import polars as pl +import numpy as np +import matplotlib.pyplot as plt + +FILE = "08102025/CombinedEndurance_0810_0817_2025.parquet" +df = pl.read_parquet(FILE) + +print(f"Raw rows in file: {df.height}") + + +def timeCol(df): + seconds = df["VDM_UTC_TIME_SECONDS"].to_numpy() + t = np.zeros(len(seconds)) + start = seconds[0] + mask = seconds == start + n = mask.sum() + t[:n] = np.linspace(0, n * (60 / 5035), n) + + idx = n + last = start + 1 + while idx < len(seconds): + if seconds[idx] != last: + chunk = idx - n + if chunk > 0: + dt = 60 / chunk + arr = np.arange(dt, 60 + dt, dt) + t[n - 1] + t[n:n + chunk] = arr[:chunk] + n += max(chunk, 1) + last += 1 + idx += 1 + + return pl.Series("Time", t) + +if "Time" not in df.columns: + df = df.with_columns(timeCol(df)) + + +df = df.filter( + (pl.col("VDM_GPS_VALID1") == 1) & + (pl.col("VDM_GPS_Latitude") != 0) & + (pl.col("VDM_GPS_Longitude") != 0) +) + +print(f"Rows with valid GPS: {df.height}") + +R = 6378137 +lat0 = np.radians(df["VDM_GPS_Latitude"][0]) +lon0 = np.radians(df["VDM_GPS_Longitude"][0]) + +lat = np.radians(df["VDM_GPS_Latitude"].to_numpy()) +lon = np.radians(df["VDM_GPS_Longitude"].to_numpy()) + +x = (lon - lon0) * np.cos(lat0) * R +y = (lat - lat0) * R + +dx = np.diff(x, prepend=x[0]) +dy = np.diff(y, prepend=y[0]) +ds = np.sqrt(dx**2 + dy**2) +s = np.cumsum(ds) + +df = df.with_columns([ + pl.Series("X", x), + pl.Series("Y", y), + pl.Series("S", s) +]) + +lap = np.zeros(len(df), dtype=int) +lap[0] = 1 +cur = 1 + +for i in range(1, len(df)): + if df["X"][i] > 80 and df["X"][i - 1] < 80: + cur += 1 + lap[i] = cur + +df = df.with_columns(pl.Series("Lap", lap)) +laps = sorted(df["Lap"].unique()) +print(f"Laps detected: {len(laps)}") + + +turn_indices = { + 1: 148819, + 4: 192977 +} + +turn_s_global = {k: float(df["S"][v]) for k, v in turn_indices.items()} + +lap_lengths = df.group_by("Lap").agg( + (pl.col("S").max() - pl.col("S").min()).alias("len") +)["len"].to_numpy() + +lap_len = np.median(lap_lengths) + +turn_s = {k: v % lap_len for k, v in turn_s_global.items()} + + +def nearest_by_s(lap_df, target): + s_rel = lap_df["S"].to_numpy() - lap_df["S"][0] + return int(np.argmin(np.abs(s_rel - target))) + + +entry_speeds = [] +durations = [] +valid_laps = [] + +for lap_no in laps: + lap_df = df.filter(pl.col("Lap") == lap_no) + if lap_df.height < 100: + continue + + s_rel = lap_df["S"].to_numpy() - lap_df["S"][0] + t_rel = lap_df["Time"].to_numpy() - lap_df["Time"][0] + + i1 = nearest_by_s(lap_df, turn_s[1]) + i4 = nearest_by_s(lap_df, turn_s[4]) + + s1, s4 = s_rel[i1], s_rel[i4] + t1, t4 = t_rel[i1], t_rel[i4] + + if s4 < s1: + s4 += lap_len + t4 += t_rel[-1] - t_rel[0] + + duration = t4 - t1 + if duration < 3 or duration > 25: + continue + + v_entry = float(lap_df["VDM_GPS_SPEED"][i1]) * 0.44704 + + entry_speeds.append(v_entry) + durations.append(duration) + valid_laps.append(lap_no) + +print(f"Valid laps used: {len(valid_laps)}") +print(f"Duration range: {min(durations):.2f}s → {max(durations):.2f}s") + + +plt.figure(figsize=(12, 7)) +plt.scatter(entry_speeds, durations, s=90, edgecolor="black") + +for i, lap in enumerate(valid_laps): + plt.text(entry_speeds[i], durations[i], f"L{lap}", fontsize=9) + +plt.xlabel("Turn 1 Entry Speed (m/s)") +plt.ylabel("Time from Turn 1 → Turn 4 (s)") +plt.title("Turn 1 Entry Speed vs T1→T4 Time (ALL LAPS, WRAP-SAFE)") +plt.grid(alpha=0.3) +plt.tight_layout() +plt.show() \ No newline at end of file