Automatic start-stop systems reduce fuel consumption by turning the engine off during idle events (for example, at traffic lights) and restarting it when propulsion or auxiliary demand requires engine power.
In FASTSim, this behavior is represented by a start-stop powertrain controller that decides whether the fuel converter remains on or is allowed to shut off at each time step.
This demo compares three cases on the same UDDS drive cycle:
A baseline conventional vehicle.
The same conventional vehicle with FASTSim’s start-stop controller enabled.
A micro-hybrid conversion with a small battery and electric machine supporting accessories and start-stop behavior.
Together, these cases separate three effects:
pure controller effect (baseline vs start-stop),
additional electrification effect (start-stop vs micro-hybrid),
combined system-level fuel-use change (baseline vs micro-hybrid).
The sections below explain what each code block is doing and why each comparison matters for modeling start-stop systems.
import fastsim
import plotly.graph_objects as go
METERS_PER_MILE = 1609.34
MJ_PER_GGE = 125.0Baseline and Start-Stop Simulations¶
This section runs two simulations with the same vehicle and cycle. The only change is enabling use_start_stop_controller() in the second case.
Conceptually, FASTSim’s start-stop controller keeps the engine on when constraints require it (for example, warm-up, minimum on-time, or accessory/charging demand), and allows engine-off idle when constraints are satisfied.
Because the test setup is controlled, the difference in fuel use can be attributed primarily to start-stop logic.
veh = fastsim.Vehicle.from_resource("2012_Ford_Fusion.yaml")
veh.set_save_interval(1)
cyc = fastsim.Cycle.from_resource("udds.csv")
sd = fastsim.SimDrive(veh, cyc)
sd.walk()
df = sd.to_dataframe()
veh_ss = fastsim.Vehicle.from_resource("2012_Ford_Fusion.yaml")
veh_ss.use_start_stop_controller()
veh_ss.set_save_interval(1)
sd_ss = fastsim.SimDrive(veh_ss, cyc)
sd_ss.walk()
df_ss = sd_ss.to_dataframe()Both runs save time-step histories to dataframes (df and df_ss).
These histories are important because start-stop is event-driven in time: you want to see when fuel power drops during stopped segments, not just a final scalar result.
Fuel Economy Comparison¶
Fuel economy is computed from two quantities: total cycle distance and cumulative fuel energy consumed by the fuel converter.
This section reports:
baseline fuel economy,
fuel economy with start-stop enabled,
percent fuel-use reduction due to start-stop.
Using fuel-energy totals is especially useful for start-stop analysis because the mechanism is reduced idle fuel burn during zero-speed periods.
cyc_dict = cyc.to_pydict()
distance_m = cyc_dict["dist_meters"][-1]
distance_mi = distance_m / METERS_PER_MILE
fuel_mj = df["veh.pt_type.Conv.fc.history.energy_fuel_joules"].iloc[-1] / 1e6
fuel_ss_mj = df_ss["veh.pt_type.Conv.fc.history.energy_fuel_joules"].iloc[-1] / 1e6
gge_gal = fuel_mj / MJ_PER_GGE
gge_ss_gal = fuel_ss_mj / MJ_PER_GGE
fuel_economy_mpg = distance_mi / gge_gal
fuel_economy_ss_mpg = distance_mi / gge_ss_gal
percent_reduction_ss = (fuel_mj - fuel_ss_mj) * 100.0 / fuel_mj
print(f"Conventional Fuel Economy: {fuel_economy_mpg:.2f} mpg")
print(f"With Start-Stop Fuel Economy: {fuel_economy_ss_mpg:.2f} mpg")
print(f"Fuel-Use Reduction (Start-Stop): {percent_reduction_ss:.2f}%")Conventional Fuel Economy: 35.42 mpg
With Start-Stop Fuel Economy: 37.55 mpg
Fuel-Use Reduction (Start-Stop): 5.66%
Micro-Hybrid Conversion¶
The function below builds a micro-hybrid electric vehicle (uHEV) representation by augmenting a conventional vehicle with:
a small reversible energy storage system (battery),
a simple constant-efficiency electric machine model,
start-stop HEV control logic,
auxiliary-load prioritization to the battery (
AuxOnResPriority).
Why this matters for start-stop modeling: a pure conventional start-stop vehicle can only turn the engine off when auxiliary and control constraints permit, while a micro-hybrid can use electrical buffering to keep accessories powered during engine-off operation more often.
def conv_to_micro_hybrid(
veh,
res_eff=None,
res_capacity_joules=None,
em_eff=None,
em_max_pwr_w=None,
allow_regen=True,
):
res_eff = 0.90 if res_eff is None else res_eff
res_capacity_joules = 72_000.0 if res_capacity_joules is None else res_capacity_joules
em_eff = 0.95 if em_eff is None else em_eff
em_max_pwr_w = 5_000.0 if em_max_pwr_w is None else em_max_pwr_w
assert res_capacity_joules > 0.0
assert 0.0 < res_eff <= 1.0
assert 0.0 < em_eff <= 1.0
veh_dict = veh.to_pydict()
res = {
"thrml": "None",
"mass_kilograms": None,
"specific_energy_joules_per_kilogram": None,
"pwr_out_max_watts": em_max_pwr_w,
"energy_capacity_joules": res_capacity_joules,
"eff_interp": {"Constant": res_eff},
"min_soc": 0.0,
"max_soc": 1.0,
"state": {
"pwr_prop_max_watts": 0.0,
"pwr_regen_max_watts": 0.0,
"pwr_disch_max_watts": 0.0,
"pwr_charge_max_watts": 0.0,
"i": 0,
"soc": 0.5,
"soc_regen_buffer": 1.0,
"soc_disch_buffer": 0.0,
"eff": 0.0,
"soh": 0.0,
"pwr_out_electrical_watts": 0.0,
"pwr_out_prop_watts": 0.0,
"pwr_aux_watts": 0.0,
"pwr_loss_watts": 0.0,
"pwr_out_chemical_watts": 0.0,
"energy_out_electrical_joules": 0.0,
"energy_out_prop_joules": 0.0,
"energy_aux_joules": 0.0,
"energy_loss_joules": 0.0,
"energy_out_chemical_joules": 0.0,
},
"history": {
"pwr_prop_max_watts": [],
"pwr_regen_max_watts": [],
"pwr_disch_max_watts": [],
"pwr_charge_max_watts": [],
"i": [],
"soc": [],
"soc_regen_buffer": [],
"soc_disch_buffer": [],
"eff": [],
"soh": [],
"pwr_out_electrical_watts": [],
"pwr_out_prop_watts": [],
"pwr_aux_watts": [],
"pwr_loss_watts": [],
"pwr_out_chemical_watts": [],
"energy_out_electrical_joules": [],
"energy_out_prop_joules": [],
"energy_aux_joules": [],
"energy_loss_joules": [],
"energy_out_chemical_joules": [],
},
"save_interval": 1,
}
em = {
"eff_interp_achieved": {
"data": {
"grid": [{"v": 1, "dim": [2], "data": [0.0, 1.0]}],
"values": {"v": 1, "dim": [2], "data": [em_eff, em_eff]},
},
"strategy": "Linear",
"extrapolate": "Error",
},
"eff_interp_at_max_input": {
"data": {
"grid": [{"v": 1, "dim": [2], "data": [0.0, 1.0]}],
"values": {"v": 1, "dim": [2], "data": [em_eff, em_eff]},
},
"strategy": "Linear",
"extrapolate": "Error",
},
"pwr_out_max_watts": em_max_pwr_w,
"specific_pwr_watts_per_kilogram": None,
"mass_kilograms": None,
"save_interval": 1,
"state": {
"i": 0,
"eff": 0.0,
"pwr_mech_fwd_out_max_watts": 0.0,
"eff_fwd_at_max_input": 0.0,
"pwr_mech_regen_max_watts": 0.0,
"eff_at_max_regen": 0.0,
"pwr_out_req_watts": 0.0,
"energy_out_req_joules": 0.0,
"pwr_elec_prop_in_watts": 0.0,
"energy_elec_prop_in_joules": 0.0,
"pwr_mech_prop_out_watts": 0.0,
"energy_mech_prop_out_joules": 0.0,
"pwr_mech_dyn_brake_watts": 0.0,
"energy_mech_dyn_brake_joules": 0.0,
"pwr_elec_dyn_brake_watts": 0.0,
"energy_elec_dyn_brake_joules": 0.0,
"pwr_loss_watts": 0.0,
"energy_loss_joules": 0.0,
},
"history": {
"i": [],
"eff": [],
"pwr_mech_fwd_out_max_watts": [],
"eff_fwd_at_max_input": [],
"pwr_mech_regen_max_watts": [],
"eff_at_max_regen": [],
"pwr_out_req_watts": [],
"energy_out_req_joules": [],
"pwr_elec_prop_in_watts": [],
"energy_elec_prop_in_joules": [],
"pwr_mech_prop_out_watts": [],
"energy_mech_prop_out_joules": [],
"pwr_mech_dyn_brake_watts": [],
"energy_mech_dyn_brake_joules": [],
"pwr_elec_dyn_brake_watts": [],
"energy_elec_dyn_brake_joules": [],
"pwr_loss_watts": [],
"energy_loss_joules": [],
},
}
pt_cntrl = {
"StartStop": {
"fc_min_time_on_seconds": None,
"soc_fc_forced_on": None,
"frac_of_most_eff_pwr_to_run_fc": None,
"temp_fc_forced_on_kelvin": None,
"temp_fc_allowed_off_kelvin": None,
"time_delay_after_stop_until_fc_can_turn_off_seconds": None,
"em_can_regen": allow_regen,
"save_interval": 1,
"state": {
"i": 0,
"fc_temperature_too_low": False,
"vehicle_not_stopped": False,
"on_time_too_short": False,
"aux_power_demand": False,
"charging_for_low_soc": False,
"time_vehicle_stopped_seconds": 0.0,
"vehicle_not_stopped_long_enough": False,
"has_traction_power_request": False,
},
"history": {
"i": [],
"fc_temperature_too_low": [],
"vehicle_not_stopped": [],
"on_time_too_short": [],
"aux_power_demand": [],
"charging_for_low_soc": [],
"time_vehicle_stopped_seconds": [],
"vehicle_not_stopped_long_enough": [],
"has_traction_power_request": [],
},
},
}
sim_params = {
"res_per_fuel_lim": 0.005,
"soc_balance_iter_err": 5,
"balance_soc": True,
"save_soc_bal_iters": False,
}
# Approximate idle fuel consumption from the conventional variant.
veh_dict["pt_type"]["Conv"]["fc"]["pwr_idle_fuel_watts"] = 11_900.0
veh_dict["pt_type"] = {
"HEV": {
"res": res,
"fs": veh_dict["pt_type"]["Conv"]["fs"],
"fc": veh_dict["pt_type"]["Conv"]["fc"],
"em": em,
"transmission": veh_dict["pt_type"]["Conv"]["transmission"],
"pt_cntrl": pt_cntrl,
"aux_cntrl": "AuxOnResPriority",
"mass_kilograms": None,
"sim_params": sim_params,
}
}
return fastsim.Vehicle.from_pydict(veh_dict)Notes on Model Simplifications¶
To keep the demo readable, the uHEV conversion uses simplified component assumptions (for example, constant electric machine efficiency and compact battery sizing).
Those simplifications are useful for isolating control effects, but they are not a substitute for full component calibration. For production-level studies, replace these assumptions with calibrated maps.
veh_uhev = conv_to_micro_hybrid(veh)
veh_uhev.set_save_interval(1)
sd_uhev = fastsim.SimDrive(veh_uhev, cyc)
sd_uhev.walk()
df_uhev = sd_uhev.to_dataframe()
fuel_uhev_mj = df_uhev["veh.pt_type.HEV.fc.history.energy_fuel_joules"].iloc[-1] / 1e6
gge_uhev_gal = fuel_uhev_mj / MJ_PER_GGE
fuel_economy_uhev_mpg = distance_mi / gge_uhev_gal
percent_reduction_uhev = (fuel_mj - fuel_uhev_mj) * 100.0 / fuel_mj
print(f"Micro-Hybrid Fuel Economy: {fuel_economy_uhev_mpg:.2f} mpg")
print(f"Fuel-Use Reduction (Micro-Hybrid): {percent_reduction_uhev:.2f}%")Micro-Hybrid Fuel Economy: 33.37 mpg
Fuel-Use Reduction (Micro-Hybrid): -6.16%
This run quantifies the additional fuel-use reduction enabled by electrified accessory support and start-stop coordination in the uHEV configuration.
Compare all three outputs together:
baseline vs start-stop: control-only effect,
start-stop vs uHEV: electrification benefit on top of start-stop,
baseline vs uHEV: total modeled benefit in this setup.
Visualizing Fuel Converter Behavior¶
The plot below combines fuel power and cycle speed in one shared-time figure.
What to look for:
Gray shaded windows mark near-stopped segments of the cycle.
In those windows, start-stop and micro-hybrid traces should show lower fuel power than baseline.
During moving segments, traces should be closer, indicating that differences are concentrated around idle-related operation.
Use the time-range slider to zoom into specific stops and compare fuel-use behavior against those cycle segments.
from plotly.subplots import make_subplots
SCENARIO_COLORS = {
"Baseline": "#0072B2",
"Start-Stop": "#D55E00",
"Micro-Hybrid": "#009E73",
}
SCENARIO_DASHES = {
"Baseline": "solid",
"Start-Stop": "dash",
"Micro-Hybrid": "dot",
}
TARGET_COLOR = "#333333"
time = df["cyc.time_seconds"]
speed_target = df["cyc.speed_meters_per_second"]
stopped_mask = speed_target <= 0.5
starts = stopped_mask & ~stopped_mask.shift(1, fill_value=False)
ends = stopped_mask & ~stopped_mask.shift(-1, fill_value=False)
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 Comparison",
"Cycle Speed and Achieved Speed",
),
)
fig.add_trace(
go.Scatter(
x=df["cyc.time_seconds"],
y=df["veh.pt_type.Conv.fc.history.pwr_fuel_watts"] / 1e3,
name="Fuel Power (Baseline)",
line={
"color": SCENARIO_COLORS["Baseline"],
"dash": SCENARIO_DASHES["Baseline"],
"width": 2.4,
},
),
row=1,
col=1,
)
fig.add_trace(
go.Scatter(
x=df_ss["cyc.time_seconds"],
y=df_ss["veh.pt_type.Conv.fc.history.pwr_fuel_watts"] / 1e3,
name="Fuel Power (Start-Stop)",
line={
"color": SCENARIO_COLORS["Start-Stop"],
"dash": SCENARIO_DASHES["Start-Stop"],
"width": 2.4,
},
),
row=1,
col=1,
)
fig.add_trace(
go.Scatter(
x=df_uhev["cyc.time_seconds"],
y=df_uhev["veh.pt_type.HEV.fc.history.pwr_fuel_watts"] / 1e3,
name="Fuel Power (Micro-Hybrid)",
line={
"color": SCENARIO_COLORS["Micro-Hybrid"],
"dash": SCENARIO_DASHES["Micro-Hybrid"],
"width": 2.4,
},
),
row=1,
col=1,
)
fig.add_trace(
go.Scatter(
x=df["cyc.time_seconds"],
y=df["cyc.speed_meters_per_second"],
name="Target Speed",
line={"dash": "dash", "width": 3, "color": TARGET_COLOR},
),
row=2,
col=1,
)
fig.add_trace(
go.Scatter(
x=df["cyc.time_seconds"],
y=df["veh.history.speed_ach_meters_per_second"],
name="Achieved Speed (Baseline)",
line={
"color": SCENARIO_COLORS["Baseline"],
"dash": SCENARIO_DASHES["Baseline"],
"width": 2.2,
},
),
row=2,
col=1,
)
fig.add_trace(
go.Scatter(
x=df_ss["cyc.time_seconds"],
y=df_ss["veh.history.speed_ach_meters_per_second"],
name="Achieved Speed (Start-Stop)",
line={
"color": SCENARIO_COLORS["Start-Stop"],
"dash": SCENARIO_DASHES["Start-Stop"],
"width": 2.2,
},
),
row=2,
col=1,
)
fig.add_trace(
go.Scatter(
x=df_uhev["cyc.time_seconds"],
y=df_uhev["veh.history.speed_ach_meters_per_second"],
name="Achieved Speed (Micro-Hybrid)",
line={
"color": SCENARIO_COLORS["Micro-Hybrid"],
"dash": SCENARIO_DASHES["Micro-Hybrid"],
"width": 2.2,
},
),
row=2,
col=1,
)
for x0, x1 in zip(time[starts], time[ends]):
fig.add_vrect(
x0=x0,
x1=x1,
fillcolor="rgba(128, 128, 128, 0.16)",
line_width=0,
layer="below",
row="all",
col=1,
)
fig.update_layout(
height=900,
title={
"text": "Start-Stop 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": 160},
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()