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.

Running Thermal Simulations

FASTSim has optional vehicle model components for thermal simulation:

  • cabin: Models cabin temperature and heat transfer

  • hvac: Models HVAC performance

  • FC thrml: Models fuel converter (e.g. internal combustion engine) thermal performance

  • RES thrml: Models reversible energy storage (i.e. traction battery) thermal performance

See Vehicles in FASTSim for a complete description of each thermal component.

Demo

Thermal behavior in FASTSim depends on both drive-cycle conditions and component thermal state at simulation start.

This demo uses one thermal HEV to show how ambient conditions and initial component temperatures influence fuel use and thermal trajectories over the same drive cycle.

This demo compares three scenarios on the same UDDS cycle:

  1. Cold soak with cold ambient.

  2. Warm component start with cold ambient.

  3. Warm component start with warm ambient.

Across these runs, FASTSim thermal behavior is driven by:

  • cycle ambient air temperature (cyc.temp_amb_air_kelvin),

  • initial cabin temperature,

  • initial battery (RES) temperature,

  • initial fuel-converter temperature.

The goal is to isolate how initial thermal state and ambient conditions change thermal transients and fuel consumption.

Note: this demo does not cover every thermal feature across all powertrains. For example, BEV-specific HVAC and BEV-only thermal behavior are not fully represented by a single HEV model.

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

# Unit conversions used for fuel-economy and temperature reporting.
METERS_PER_MILE = 1609.34
MJ_PER_GGE = 125.0
KELVIN_OFFSET = 273.15

Define Thermal Scenarios

All scenarios use the same HEV and cycle. Only ambient and initial temperatures change.

Scenarios:

  1. Cold soak, cold ambient.

  2. Warm start, cold ambient.

  3. Warm start, warm ambient.

scenario_temps_c = {
    # Cold soak case: all components start near ambient.
    "Cold Soak / Cold Ambient": {
        "temp_amb_c": -6.7,
        "temp_cabin_c": -6.7,
        "temp_res_c": -6.7,
        "temp_fc_c": -6.7,
    },
    # Warm cabin/battery and hot fuel converter in cold air.
    "Warm Start / Cold Ambient": {
        "temp_amb_c": -6.7,
        "temp_cabin_c": 22.0,
        "temp_res_c": 22.0,
        "temp_fc_c": 70.0,
    },
    # Warm start with warm ambient to contrast cold-weather behavior.
    "Warm Start / Warm Ambient": {
        "temp_amb_c": 38.0,
        "temp_cabin_c": 45.0,
        "temp_res_c": 45.0,
        "temp_fc_c": 45.0,
    },
}


def c_to_k(temp_c):
    # FASTSim thermal states are specified in kelvin.
    return temp_c + KELVIN_OFFSET

Apply Initial Temperatures and Run Simulations

The helper below edits the thermal HEV state before simulation by writing temperature values into the vehicle dictionary and ambient values into the cycle dictionary.

THERMAL_HEV_RESOURCE = "2021_Hyundai_Sonata_Hybrid_Blue_thrml.yaml"

# Dataframe columns used for summary metrics and plots.
CABIN_TEMP_COL = "veh.cabin.LumpedCabin.history.temperature_kelvin"
RES_TEMP_COL = "veh.pt_type.HEV.res.thrml.RESLumpedThermal.history.temperature_kelvin"
FC_TEMP_COL = "veh.pt_type.HEV.fc.thrml.FuelConverterThermal.history.temperature_kelvin"
AMBIENT_TEMP_COL = "cyc.temp_amb_air_kelvin"
FUEL_PWR_COL = "veh.pt_type.HEV.fc.history.pwr_fuel_watts"
FUEL_ENERGY_COL = "veh.pt_type.HEV.fc.history.energy_fuel_joules"
SPEED_COL = "veh.history.speed_ach_meters_per_second"
TARGET_SPEED_COL = "cyc.speed_meters_per_second"
TIME_COL = "cyc.time_seconds"


def run_thermal_hev_case(name, temps_c):
    # Start from the same thermal HEV template for each scenario.
    veh_dict = fastsim.Vehicle.from_resource(THERMAL_HEV_RESOURCE).to_pydict()

    # Set initial component temperatures (cabin, battery/RES, fuel converter).
    veh_dict["cabin"]["LumpedCabin"]["state"]["temperature_kelvin"] = c_to_k(
        temps_c["temp_cabin_c"]
    )
    veh_dict["pt_type"]["HEV"]["res"]["thrml"]["RESLumpedThermal"]["state"][
        "temperature_kelvin"
    ] = c_to_k(temps_c["temp_res_c"])
    veh_dict["pt_type"]["HEV"]["fc"]["thrml"]["FuelConverterThermal"]["state"][
        "temperature_kelvin"
    ] = c_to_k(temps_c["temp_fc_c"])
    veh = fastsim.Vehicle.from_pydict(veh_dict)
    # Save full time-series history so thermal trajectories are available in outputs.
    veh.set_save_interval(1)

    # Apply scenario ambient temperature to every cycle time step.
    cyc_dict = fastsim.Cycle.from_resource("udds.csv").to_pydict()
    cyc_dict["temp_amb_air_kelvin"] = [c_to_k(temps_c["temp_amb_c"])] * len(
        cyc_dict["time_seconds"]
    )
    cyc = fastsim.Cycle.from_pydict(cyc_dict)

    # Run FASTSim and capture time histories for comparison.
    sd = fastsim.SimDrive(veh, cyc)
    sd.walk()
    df = sd.to_dataframe()

    # Compute cycle-level fuel economy from distance and cumulative fuel energy.
    distance_m = cyc_dict["dist_meters"][-1]
    distance_mi = distance_m / METERS_PER_MILE
    fuel_mj = df[FUEL_ENERGY_COL].iloc[-1] / 1e6
    mpg = distance_mi / (fuel_mj / MJ_PER_GGE)

    return {
        "name": name,
        "temps_c": temps_c,
        "df": df,
        "distance_mi": distance_mi,
        "fuel_mj": fuel_mj,
        "mpg": mpg,
    }


# Run all defined temperature scenarios into a common results dictionary.
results = {name: run_thermal_hev_case(name, temps) for name, temps in scenario_temps_c.items()}

Summary Metrics

This table compares fuel economy and final temperatures across the three initial-condition cases.

summary_rows = []

# Build one summary row per scenario for side-by-side comparison.
for name, r in results.items():
    df = r["df"]
    summary_rows.append(
        {
            "Scenario": name,
            "Ambient [C]": r["temps_c"]["temp_amb_c"],
            "Init Cabin [C]": r["temps_c"]["temp_cabin_c"],
            "Init RES [C]": r["temps_c"]["temp_res_c"],
            "Init FC [C]": r["temps_c"]["temp_fc_c"],
            "Fuel Economy [mpg]": round(r["mpg"], 2),
            "Final Cabin [C]": round(df[CABIN_TEMP_COL].iloc[-1] - KELVIN_OFFSET, 2),
            "Final RES [C]": round(df[RES_TEMP_COL].iloc[-1] - KELVIN_OFFSET, 2),
            "Final FC [C]": round(df[FC_TEMP_COL].iloc[-1] - KELVIN_OFFSET, 2),
        }
    )

# Display the comparison table.
pd.DataFrame(summary_rows)
Loading...

Fuel Use by Segment

This combined figure aligns fuel power and speed on one shared time axis so you can inspect where temperature-dependent behavior influences fuel use during the cycle.

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",
        "Target and Achieved Speed",
    ),
)

scenario_palette = ["#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00", "#000000"]
scenario_dash_cycle = ["solid", "dash", "dot", "dashdot", "longdash", "longdashdot"]
scenario_colors = {
    name: scenario_palette[idx % len(scenario_palette)] for idx, name in enumerate(results.keys())
}
scenario_dashes = {
    name: scenario_dash_cycle[idx % len(scenario_dash_cycle)]
    for idx, name in enumerate(results.keys())
}
target_color = "#333333"

# Top panel: fuel power traces for each thermal scenario.
for name, r in results.items():
    df = r["df"]
    fig.add_trace(
        go.Scatter(
            x=df[TIME_COL],
            y=df[FUEL_PWR_COL] / 1e3,
            name=f"Fuel Power ({name})",
            line={"color": scenario_colors[name], "dash": scenario_dashes[name], "width": 2.3},
        ),
        row=1,
        col=1,
    )

# Bottom panel reference: target cycle speed (same for all scenarios).
baseline_df = next(iter(results.values()))["df"]
fig.add_trace(
    go.Scatter(
        x=baseline_df[TIME_COL],
        y=baseline_df[TARGET_SPEED_COL],
        name="Target Speed",
        line={"dash": "dash", "width": 3, "color": target_color},
    ),
    row=2,
    col=1,
)

# Bottom panel overlays: achieved speed for each scenario.
for name, r in results.items():
    df = r["df"]
    fig.add_trace(
        go.Scatter(
            x=df[TIME_COL],
            y=df[SPEED_COL],
            name=f"Achieved Speed ({name})",
            line={"color": scenario_colors[name], "dash": scenario_dashes[name], "width": 2.1},
        ),
        row=2,
        col=1,
    )

fig.update_layout(
    height=900,
    title={
        "text": "HEV Thermal Scenarios: Fuel Use vs Cycle Segments",
        "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": 210},
    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)

# Interactive figure for segment-level interpretation.
fig.show()
Loading...

Thermal State Trajectories

This figure compares cabin, battery, and fuel-converter temperatures across scenarios, with ambient temperature shown as a dashed reference.

A fourth panel shows achieved speed so the rangeslider preview reflects cycle-speed segments while you inspect thermal transients.

fig_t = make_subplots(
    rows=4,
    cols=1,
    shared_xaxes=True,
    vertical_spacing=0.06,
    row_heights=[0.29, 0.25, 0.25, 0.21],
    subplot_titles=(
        "Cabin Temperature",
        "Battery (RES) Temperature",
        "Fuel Converter Temperature",
        "Achieved Speed",
    ),
)

scenario_palette = ["#0072B2", "#D55E00", "#009E73", "#CC79A7", "#E69F00", "#000000"]
scenario_dash_cycle = ["solid", "dash", "dot", "dashdot", "longdash", "longdashdot"]
scenario_colors = {
    name: scenario_palette[idx % len(scenario_palette)] for idx, name in enumerate(results.keys())
}
scenario_dashes = {
    name: scenario_dash_cycle[idx % len(scenario_dash_cycle)]
    for idx, name in enumerate(results.keys())
}

# Plot cabin, battery, fuel-converter temperatures, and achieved speed for each scenario.
for name, r in results.items():
    df = r["df"]
    t = df[TIME_COL]
    scenario_color = scenario_colors[name]
    scenario_dash = scenario_dashes[name]

    fig_t.add_trace(
        go.Scatter(
            x=t,
            y=df[CABIN_TEMP_COL] - KELVIN_OFFSET,
            name=f"Cabin ({name})",
            line={"color": scenario_color, "dash": scenario_dash, "width": 2.2},
        ),
        row=1,
        col=1,
    )
    fig_t.add_trace(
        go.Scatter(
            x=t,
            y=df[RES_TEMP_COL] - KELVIN_OFFSET,
            name=f"RES ({name})",
            line={"color": scenario_color, "dash": scenario_dash, "width": 2.2},
        ),
        row=2,
        col=1,
    )
    fig_t.add_trace(
        go.Scatter(
            x=t,
            y=df[FC_TEMP_COL] - KELVIN_OFFSET,
            name=f"FC ({name})",
            line={"color": scenario_color, "dash": scenario_dash, "width": 2.2},
        ),
        row=3,
        col=1,
    )
    fig_t.add_trace(
        go.Scatter(
            x=t,
            y=df[SPEED_COL],
            name=f"Speed ({name})",
            line={"color": scenario_color, "dash": scenario_dash, "width": 2.0},
        ),
        row=4,
        col=1,
    )
    # Overlay ambient on cabin panel as a dashed reference.
    fig_t.add_trace(
        go.Scatter(
            x=t,
            y=df[AMBIENT_TEMP_COL] - KELVIN_OFFSET,
            name=f"Ambient ({name})",
            line={"dash": "longdash", "color": scenario_color, "width": 1.6},
            opacity=0.5,
        ),
        row=1,
        col=1,
    )

fig_t.update_layout(
    height=1250,
    title={
        "text": "HEV Thermal Scenarios: Component Temperature Trajectories",
        "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": 260},
    hovermode="x unified",
)
fig_t.update_yaxes(title_text="Cabin [C]", row=1, col=1)
fig_t.update_yaxes(title_text="RES [C]", row=2, col=1)
fig_t.update_yaxes(title_text="FC [C]", row=3, col=1)
fig_t.update_yaxes(title_text="Speed [m/s]", row=4, col=1)
fig_t.update_xaxes(title_text="Time [s]", row=4, col=1, rangeslider_visible=True)

# Interactive temperature trajectories across scenarios.
fig_t.show()
Loading...