FASTSim has optional vehicle model components for thermal simulation:
cabin: Models cabin temperature and heat transferhvac: Models HVAC performanceFC
thrml: Models fuel converter (e.g. internal combustion engine) thermal performanceRES
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:
Cold soak with cold ambient.
Warm component start with cold ambient.
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.15Define Thermal Scenarios¶
All scenarios use the same HEV and cycle. Only ambient and initial temperatures change.
Scenarios:
Cold soak, cold ambient.
Warm start, cold ambient.
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_OFFSETApply 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)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()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()