Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Modeling Connected and Automated Vehicles (CAVs)

This notebook demonstrates how FASTSim can model longitudinal CAV behaviors by modifying the drive-cycle trace before running energy simulation.

In FASTSim, CAV behavior in this context is represented through maneuver logic that adjusts vehicle speed trajectories, including:

  • eco-cruise with IDM-style speed regulation,

  • predictive coasting with configurable braking and look-ahead settings,

  • combined cruise + coast operation.

This demo evaluates four scenarios on the same starting cycle and vehicle:

  1. Baseline (no CAV maneuvering).

  2. Eco-cruise (IDM).

  3. Advanced coasting.

  4. Eco-cruise + coasting together.

The goal is to compare how these controls reshape speed and fuel use, and to inspect which cycle segments drive the differences.

import fastsim
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots

METERS_PER_MILE = 1609.34
MJ_PER_GGE = 125.0

Modeling Approach

All scenarios start from the same vehicle and base cycle. The only differences are maneuver controls applied through Maneuver settings.

FASTSim provides these longitudinal-control levers for this demo:

  • IDM parameters (idm_*) to regulate target-following behavior and smooth speed response,

  • coasting parameters (coast_*) to trigger earlier/predictive deceleration behavior,

  • combined use of IDM and coasting in a single maneuver pipeline.

After maneuvers are applied, each modified cycle is simulated with SimDrive and compared on fuel and speed outcomes.

Scenario Setup

Load one vehicle and one base cycle, then create an extended cycle so maneuver effects are easier to observe over longer segments.

veh_template = fastsim.Vehicle.from_resource("2012_Ford_Fusion.yaml")

cyc_base = fastsim.Cycle.from_resource("udds.csv")
ending_idle_s = cyc_base.ending_idle_time_s()

# Extend cycle duration so cruise/coast behavior is easier to compare.
cyc_seed = cyc_base.extend_time(absolute_time_s=180.0, time_fraction=0.28)
desired_speed_mps = cyc_seed.average_speed_m_per_s(while_moving=True)

Run Four CAV Scenarios

The helper below applies maneuver settings and runs FASTSim for each scenario: baseline, eco-cruise only, coasting only, and combined eco-cruise + coasting.

def run_cav_scenario(label, enable_idm=False, enable_coast=False):
    veh = veh_template.copy()

    if not enable_idm and not enable_coast:
        cyc_run = cyc_seed.trim_ending_idle(idle_to_keep_s=ending_idle_s)
        man = None
    else:
        man = fastsim.Maneuver.create_from(cyc_seed, veh.copy())
        d = man.to_pydict()

        if enable_coast:
            d["coast_allow"] = True
            d["coast_brake_start_speed_meters_per_second"] = 8.9408
            d["coast_brake_accel_meters_per_second_squared"] = -2.5
            d["favor_grade_accuracy"] = True
            d["coast_allow_passing"] = True
            d["coast_max_speed_meters_per_second"] = 33.5280
            d["coast_time_horizon_for_adjustment_seconds"] = 120.0

        if enable_idm:
            d["idm_allow"] = True
            d["idm_desired_speed_meters_per_second"] = desired_speed_mps
            d["idm_headway_seconds"] = 1.0
            d["idm_minimum_gap_meters"] = 1.0
            d["idm_delta"] = 4.0
            d["idm_acceleration_meters_per_second_squared"] = 1.0
            d["idm_deceleration_meters_per_second_squared"] = 2.5

        man = fastsim.Maneuver.from_pydict(d)
        cyc_run = man.apply_maneuvers().trim_ending_idle(idle_to_keep_s=ending_idle_s)

    sd = fastsim.SimDrive(veh, cyc_run)
    sd.walk()
    df = sd.to_dataframe()

    cyc_dict = cyc_run.to_pydict()
    dist_m = cyc_dict["dist_meters"][-1]
    dist_mi = dist_m / METERS_PER_MILE
    fuel_mj = df["veh.pt_type.Conv.fc.history.energy_fuel_joules"].iloc[-1] / 1e6
    fuel_gal = fuel_mj / MJ_PER_GGE
    mpg = dist_mi / fuel_gal

    return {
        "label": label,
        "maneuver": man,
        "cycle": cyc_run,
        "df": df,
        "distance_mi": dist_mi,
        "fuel_mj": fuel_mj,
        "mpg": mpg,
    }

results = {
    "Baseline": run_cav_scenario("Baseline", enable_idm=False, enable_coast=False),
    "Eco-Cruise (IDM)": run_cav_scenario("Eco-Cruise (IDM)", enable_idm=True, enable_coast=False),
    "Advanced Coasting": run_cav_scenario("Advanced Coasting", enable_idm=False, enable_coast=True),
    "Cruise + Coast": run_cav_scenario("Cruise + Coast", enable_idm=True, enable_coast=True),
}

Fuel Economy Comparison

This table compares each scenario’s distance-normalized fuel use. Baseline is included to make relative gains easy to read.

summary_rows = []
baseline_fuel_mj = results["Baseline"]["fuel_mj"]

for name, r in results.items():
    fuel_reduction_pct = (baseline_fuel_mj - r["fuel_mj"]) * 100.0 / baseline_fuel_mj
    summary_rows.append({
        "Scenario": name,
        "Distance [mi]": round(r["distance_mi"], 3),
        "Fuel [MJ]": round(r["fuel_mj"], 3),
        "Fuel Economy [mpg]": round(r["mpg"], 2),
        "Fuel Reduction vs Baseline [%]": round(fuel_reduction_pct, 2),
    })

pd.DataFrame(summary_rows)
Loading...

Segment-Level Comparison

The figure below combines fuel power and speed traces in one shared-time layout so you can inspect where each strategy diverges.

How to read it:

  • Top panel: fuel power for all four scenarios.

  • Bottom panel: target speed and achieved speeds.

  • Green shaded windows: segments where the combined Cruise + Coast maneuver reports coasting active.

Use the range slider to zoom into any event and compare strategy behavior at that segment.

fig = make_subplots(
    rows=2,
    cols=1,
    shared_xaxes=True,
    vertical_spacing=0.08,
    row_heights=[0.58, 0.42],
    subplot_titles=(
        "Fuel Converter Fuel Power by Scenario",
        "Target and Achieved Speed by Scenario",
    ),
)

scenario_palette = ["#0072B2", "#56B4E9", "#009E73", "#E69F00", "#D55E00", "#CC79A7"]
scenario_colors = {
    name: scenario_palette[idx % len(scenario_palette)]
    for idx, name in enumerate(results.keys())
}
target_color = "#4D4D4D"

for name, r in results.items():
    df_s = r["df"]
    fig.add_trace(
        go.Scatter(
            x=df_s["cyc.time_seconds"],
            y=df_s["veh.pt_type.Conv.fc.history.pwr_fuel_watts"] / 1e3,
            name=f"Fuel Power ({name})",
            line={"color": scenario_colors[name]},
        ),
        row=1,
        col=1,
    )

baseline_df = results["Baseline"]["df"]
fig.add_trace(
    go.Scatter(
        x=baseline_df["cyc.time_seconds"],
        y=baseline_df["cyc.speed_meters_per_second"],
        name="Target Speed",
        line={"dash": "dash", "width": 3, "color": target_color},
    ),
    row=2,
    col=1,
)

for name, r in results.items():
    df_s = r["df"]
    fig.add_trace(
        go.Scatter(
            x=df_s["cyc.time_seconds"],
            y=df_s["veh.history.speed_ach_meters_per_second"],
            name=f"Achieved Speed ({name})",
            line={"color": scenario_colors[name]},
        ),
        row=2,
        col=1,
    )

combined = results["Cruise + Coast"]
if combined["maneuver"] is not None:
    coast_raw = np.array(combined["maneuver"].is_coasting(), dtype=float)
    df_combined = combined["df"]
    t_combined = np.array(df_combined["cyc.time_seconds"], dtype=float)
    d_combined = np.array(df_combined["cyc.dist_meters"], dtype=float)

    cyc_combined = combined["cycle"].to_pydict()
    d_maneuver = np.array(cyc_combined["dist_meters"], dtype=float)

    n = min(len(d_maneuver), len(coast_raw))
    if n > 1:
        coast_interp = np.interp(d_combined, d_maneuver[:n], coast_raw[:n])
        coast_mask = pd.Series(coast_interp >= 0.5)
        starts = coast_mask & ~coast_mask.shift(1, fill_value=False)
        ends = coast_mask & ~coast_mask.shift(-1, fill_value=False)

        for x0, x1 in zip(t_combined[starts.values], t_combined[ends.values]):
            fig.add_vrect(
                x0=x0,
                x1=x1,
                fillcolor="rgba(46, 204, 113, 0.16)",
                line_width=0,
                layer="below",
                row="all",
                col=1,
            )

fig.update_layout(
    height=900,
    title={"text": "CAV Scenario Comparison: Fuel Use and Speed by Segment", "x": 0.5, "xanchor": "center", "y": 0.98, "yanchor": "top"},
    legend={"orientation": "h", "yanchor": "bottom", "y": 1.06, "xanchor": "left", "x": 0.0},
    margin={"t": 180},
    hovermode="x unified",
)
fig.update_yaxes(title_text="Fuel Power [kW]", row=1, col=1)
fig.update_yaxes(title_text="Speed [m/s]", row=2, col=1)
fig.update_xaxes(title_text="Time [s]", row=2, col=1, rangeslider_visible=True)

fig.show()
Loading...