SuboptimumG Tutorial#

This notebook will showcase how to use the key features of SuboptimumG. Note that if you ever modify the source code of the package. You have to restart the kernel so that your changes are detected! (Hit the “Restart” button on VSCode).

Simulating a Single Competition#

We start by simulating the results of a single competition with the default car (REV X). We load in competition-wide parameters (tracks, competitor statistics, simulation hyperparameters, etc.) from competition.yaml. We load in car-specific parameters from car.yaml. Check these yaml files to see what SuboptimumG currently takes into account and to edit the defaults.

Observe that there are soft plausibility warnings when a parameter is outside of an expected range.

[ ]:
from suboptimumg.yaml import load_competition_from_yaml
from suboptimumg.gdrive import load_competition_from_gdrive

# Create sample competitions
michigan2023_revX = load_competition_from_yaml(
    car_yaml_path="../parameters/car.yaml",
    comp_yaml_path="../parameters/competition.yaml",
)
michigan2023_awd = load_competition_from_yaml(
    car_yaml_path="../parameters/awd_car.yaml",
    comp_yaml_path="../parameters/competition.yaml",
)

revx_comp_data = michigan2023_revX.to_data()
awd_comp_data = michigan2023_awd.to_data()

# We can also optionally load parameters from published Google Sheets
michigan2023_revX_alt = load_competition_from_gdrive(
    car_config_url="https://docs.google.com/spreadsheets/d/e/2PACX-1vQRPeSToFhhcMYo4J12FvPuIoCv0xAVCvcOlb-SlRxRleQQJNwq7Jiw1H0Hwr6oYOFBcCD_422CosnJ/pub?output=csv",
    comp_config_url="https://docs.google.com/spreadsheets/d/e/2PACX-1vSPaKIHa64Buiqw_2yk4P3iP3PlsNjuE-GjYSHQDenn6dFYG_EelNSg7oqwH5mEvXzSNfB7j1p42H12/pub?output=csv",
)

We run the competition by calling .run() on the competition object. Then, we can print out our point totals.

[ ]:
from suboptimumg.compsim import pretty_print_results

# Run sample competition
comp_results = michigan2023_revX.run(extract_internal_data=True)
awd_results = michigan2023_awd.run(extract_internal_data=True)

pretty_print_results(comp_results)
pretty_print_results(awd_results)

We have tools to graph more detailed information about the behavior of the car across different events. Here’s an example for the Endurance event. We plot what is simulated for a single “seed”, which is a radii minima on the Endurance track. Seeds are used as starting points in our simulation.

[ ]:
endu_res = comp_results.endurance
endu_awd = awd_results.endurance
autoX_res = comp_results.autoX
autoX_awd = awd_results.autoX
[ ]:
from suboptimumg.plotting.internal_data_plots import plot_per_seed, Axis

# endu_res = comp_results.endurance

plot_per_seed(
    results=endu_res.lapsim_results,
    x_axis=Axis.DISTANCE,
    include_indices=range(4),
    include_power=True,
)

We can also display detailed simulation results for an event, plotting velocity, acceleration, and power at every step across a lap.

[ ]:
from suboptimumg.plotting.internal_data_plots import plot_event_traces, Metric

plot_event_traces(
    results=endu_res.lapsim_results,
    x_axis=Axis.DISTANCE,
    metrics=[Metric.VELOCITY, Metric.ACCEL, Metric.POWER],
)
[ ]:
from suboptimumg.plotting.internal_data_plots import plot_event_traces, Metric

plot_event_traces(
    # results=[endu_res.lapsim_results, endu_awd.lapsim_results],
    results=[autoX_res.lapsim_results, autoX_awd.lapsim_results],
    x_axis=Axis.DISTANCE,
    metrics=[Metric.VELOCITY, Metric.ACCEL, Metric.POWER],
)

For a more compact but comprehensive view, we can plot these traces across all events in a competition.

[ ]:
from suboptimumg.plotting.internal_data_plots import plot_competition_traces

plot_competition_traces(
    # competitions=comp_results,
    competitions=[comp_results, awd_results],
    x_axis=Axis.DISTANCE,
    labels=["RWD", "AWD"],
)

If needed, it is also fairly simple to do custom analysis on the data. We store our data in numpy datatypes, which should be compatible with most external libraries.

[ ]:
import numpy as np
from suboptimumg.plotting.generic_plot import plot2D

plot2D(
    x_list=np.array(endu_res.lapsim_results.lap_t),
    y_list=np.array(endu_res.lapsim_results.lap_powers) / 1000,
    title="Power Usage over a Single Endurance Lap",
    x_axis="Time since Lap Start (s)",
    y_axis="Power (kW)",
    theme="powertrain",
)

print("Power Statistics:")
print(f"Max Power: {np.max(endu_res.lapsim_results.lap_powers) / 1000} kW")
print(f"Min Power: {np.min(endu_res.lapsim_results.lap_powers) / 1000} kW")
print(f"Avg Power: {np.mean(endu_res.lapsim_results.lap_powers) / 1000} kW")

We can also use custom functions to visualize the tracks that were used as part of the simulation. map_color and map_3d are function built on top of track_vis that map a second variable onto the track, to show the simulated instantaneous velocity and power consumption of the car at every point.

[ ]:
fig, ax = michigan2023_revX.endurance.plot_arcs()
fig
[ ]:
fig = michigan2023_revX.endurance.plot_with_overlay(
    variable=endu_res.lapsim_results.lap_vels,
    label="Speed (m/s)",
    theme="thermal",
)
fig.show()
fig.write_html("endurance_speed_overlay.html")
[ ]:
fig = michigan2023_revX.endurance.plot_3d(
    variable=endu_res.lapsim_results.lap_vels,
    title="Speed over Endurance Lap",
    z_label="Speed (m/s)",
    theme="thermal",
)
fig.show()

Sweeps#

Now we move on to the Sweep functionality, an abstraction built on top of single competition simulations, that allow us to vary one or two parameters, and observe how that affects competition results.

We first define a sweep variable by creating a Python dictionary with four items. “variable” is the name of the variable. To get the correct name, take reference from the names in Car.yaml, and nest parameters using dots. For example, the MOC efficiency should be referred to as “pwrtn.motor.moc_efficiency”, and the mass of the car is just “mass”.

Observe that plausibility checks are done on the car configurations at both bounds of the sweep. In a 2-variable sweep, four sets of bounds are checked for plausibility.

[ ]:
from suboptimumg.sweep import *

sweep_var_1 = SweepParamConfig(
    name="mass",
    min=200,
    max=300,
    steps=50,
)
mass_sweep = create_sweeper(comp_data=revx_comp_data, var_1=sweep_var_1)
mass_results = mass_sweep.sweep()

Calling .sweep() on the Sweeper class object returns a SweepResults object. We can produce graphs with this SweepResults object. By default, the y-variable in the graph is the total number of points earned in a competition. Optionally, we can also choose to graph other y-variables such as event times and event points by passing arguments like y_var=”accel_pts” (indicating Accel event points) or y_var=”endurance_t” (indicating time taken to complete Endurance event).

[ ]:
fig = mass_results.plot(
    x_label="Mass (kg)",
    y_label="Total Points",
    title="Mass Sweep - Total Points",
)
fig.write_html("mass_sweep_total_points.html")
fig.show()

We can also compute interpolated points/times (y_var) and first derivatives for any given x-value within range of the sweep.

[ ]:
print(f"Total points @ mass=257kg: {mass_results.y_at_x(x=257)}")
print(
    f"Change in points for every 1kg increase in mass @ mass=257kg: {mass_results.dydx_at_x(x=257)}"
)

The Sweeper class object also supports 2-parameter sweeps.

[ ]:
from suboptimumg.sweep import *

sweep_var_1 = SweepParamConfig(
    name="aero.cl",
    min=2.8,
    max=3.8,
    steps=15,
)

sweep_var_2 = SweepParamConfig(
    name="aero.cd",
    min=1.0,
    max=2.0,
    steps=15,
)

aero_sweep = create_sweeper(
    comp_data=revx_comp_data,
    var_1=sweep_var_1,
    var_2=sweep_var_2,
)
aero_results = aero_sweep.sweep()

For 2-parameter sweep results, we can likewise compute interpolated points/times (z_var) and partial derivatives at any given pair of parameters within the sweep range.

[ ]:
# x refers to var_1 (aero.cl in this case) and y refers to var_2 (aero.cd in this case)
print(f"Total points @ cl=3.55, cd=1.65: {aero_results.z_at_xy(x=3.55, y=1.65)}")
print(
    f"Additional point gain for every 1.0 increase in cl @ cl=3.55, cd=1.65: {aero_results.dzdx_at_xy(x=3.55, y=1.65)}"
)
print(
    f"Additional point gain for every 1.0 increase in cd @ cl=3.55, cd=1.65: {aero_results.dzdy_at_xy(x=3.55, y=1.65)}"
)

For 2-parameter sweep results, we have a choice between two types of graphs to represent the data. Call either .plot_contour() or .plot_surface(). Again, the target variable is by default total points, but can be controlled by passing an argument like z_var=”endurance_pts”.

[ ]:
fig = aero_results.plot_contour(
    x_label="CL", y_label="CD", z_label="Total Points", title="CL vs CD Sweep"
)
fig.write_html("aero_sweep_contour.html")
fig.show()
[ ]:
from suboptimumg.plotting import PlotType

aero_results.grid_plot(
    # show_event_times=True,
    plot_type=PlotType.CONTOUR,
)
[ ]:
from suboptimumg.plotting import SceneConfig

fig = aero_results.plot_surface(
    x_label="CL",
    y_label="CD",
    z_label="Total Points",
    title="CL vs CD Sweep",
    scene_config=SceneConfig(default_view_angle=120),
)
fig.write_html("aero_sweep_surface.html")
fig.show()

We can do a full vehicle parameter sweep by using the ArraySweeper class

This takes in the following parameters:

  • comp_config: competition-wide parameters

  • default_car_config: default car parameters

  • var_list: list of variables to sweep e.g. [“mass”, “pwrtn.motor.moc_efficiency”]

  • var_percent_step_list: list of percent steps to sweep default is: [-0.1, -0.08, -0.06, -0.04, -0.02, 0, 0.02, 0.04, 0.06, 0.08, 0.1]

[ ]:
from suboptimumg.sweep import ArraySweeper

arr_sweep_params = [
    "mass",
    "w_distr_b",
    "cg_h",
    "wb",
    "front_track",
    "rear_track",
    "aero.cl",
    "aero.cd",
    "aero.front_area",
    "aero.cop",
    "pwrtn.ratio",
]
arr_sweep = ArraySweeper(comp_data=revx_comp_data, var_list=arr_sweep_params)
arr_sweep_results = arr_sweep.sweep(verbose=True)
[ ]:
fig = arr_sweep_results.plot(
    y_label="Total Points",
    title="Array Sweep - Total Points",
)
fig.write_html("array_sweep_total_points.html")
fig.show()