Skip to content

Vehicle-system drive cycles

aeolus1d carries a vehicle-system layer alongside the 1-D gas-network solver: a longitudinal vehicle model, a closed-loop driver, drive-cycle profiles, a transmission/driveline with an AMT shift state machine, a parallel-hybrid powertrain, and a control/signal toolbox. This is the AVL Cruise M-style vehicle workflow — feed a speed-time target, let a driver model track it, and read back the cycle KPIs (distance, tracking error, fuel, CO₂).

A vehicle-system run is not a 1-D gas-network solve, so it has its own self-contained case format and CLI entry point:

aeolus1d drive-cycle examples/85_vehicle_drive_cycle.toml --out trace.csv

aeolus1d run is for gas-network cases; aeolus1d drive-cycle is for vehicle-system cases. They do not share a schema.

The case file

Four blocks, all optional except [drive_cycle]:

[vehicle]                     # aeolus1d.vehicle.VehicleParameters fields
mass_kg = 1300.0
cd = 0.30
frontal_area_m2 = 2.2
wheel_radius_m = 0.30
gear_ratio = 8.0              # combined gearbox × final drive
driveline_efficiency = 0.92
crr = 0.010                   # rolling-resistance coefficient

[driver]                      # aeolus1d.driver.DriverParameters fields
kp = 2.0                      # P gain on speed error  [1/s]
ki = 0.5                      # I gain                 [1/s²]
feedforward = true            # add steady road-load; PI only trims

[drive_cycle]
source = "builtin"            # "builtin" | "csv" | "segments"
name = "urban_illustrative"
dt_s = 0.1                    # keep kp*dt_s < 1 (PI pole 1 - kp*dt_s)
grade_rad = 0.0

[report]                      # optional — turns wheel work into fuel/CO₂
bsfc_g_per_kwh = 240.0
fuel_density_kg_l = 0.745      # gasoline default
fuel_co2_g_per_g = 3.09        # gasoline default (g CO₂ per g fuel)

Unknown keys in [vehicle], [driver], and [report] raise an error (typo protection) rather than being silently ignored. The only three keys [report] accepts are bsfc_g_per_kwh, fuel_density_kg_l, and fuel_co2_g_per_g; the last two default to gasoline (0.745 kg/L, 3.09 g CO₂/g) and only matter once a BSFC is supplied.

Choosing a cycle

The [drive_cycle] source selects how the speed-time profile is built:

source What it does Use for
"builtin" A bundled illustrative cycle by name (urban_illustrative, highway_illustrative). Demos / smoke tests. Not regulatory.
"csv" Load a two-column time,speed table from path (speed_unit = "kmh" or "mps"; column indices default to time_col = 0, speed_col = 1, and a header row is assumed via has_header = true). Official WLTP / NEDC / FTP-75 / JC08 cycles.
"segments" Build an exact synthetic cycle from inline idle/accelerate/cruise/decelerate phases. Reproducible custom cycles.

Regulatory cycles are not bundled. The WLTP / NEDC / FTP-75 / JC08 speed-time tables are public data but long empirical traces; aeolus1d does not ship them (and won't fabricate them). Drop the official CSV next to your case and point source = "csv", path = "..." at it. Naming a regulatory cycle under source = "builtin" returns a pointed error telling you to use the CSV path.

A segments cycle is defined inline:

[drive_cycle]
source = "segments"
name = "my_synthetic"
dt_s = 0.1
segments = [
  { kind = "idle",       duration_s = 8.0 },
  { kind = "accelerate", duration_s = 12.0, target_kmh = 50.0 },
  { kind = "cruise",     duration_s = 30.0, target_kmh = 50.0 },
  { kind = "decelerate", duration_s = 10.0, target_kmh = 0.0 },
]

What it reports

aeolus1d drive-cycle prints the cycle KPIs and, with --out, writes the per-step trace (t_s, v_target_mps, v_actual_mps, engine_torque_Nm, brake_force_N) as CSV:

Drive cycle 'urban_illustrative': 150 s, dt=0.1 s
  distance         :    0.777 km
  avg speed        :    18.64 km/h
  max speed        :    30.99 km/h
  tracking RMSE    :    0.164 m/s
  positive wheel W :    0.067 kWh
  fuel             :    3.007 L/100km     # only with [report] bsfc
  CO₂              :    69.22 g/km

Fuel and CO₂ are estimated from the positive engine-side work (the wheel work divided by the driveline efficiency) via the supplied brake-specific fuel consumption (bsfc_g_per_kwh) — a first-order energy-balance estimate, not a fuel map. For a fuel map, drive the engine model directly (see the engine validation suites).

Electrified (hybrid) drive cycles — W5

Add a [powertrain] block and the same case becomes a parallel hybrid: the driver's per-step engine-torque demand is split between the engine and an e-motor by a power-split controller, and the battery state-of-charge is coulomb-counted. aeolus1d drive-cycle auto-detects the block and reports the W5 deliverable — SoC, electrical energy used (and regen recovered), and the engine-share fuel + tailpipe CO₂:

[powertrain]
  [powertrain.motor]      # aeolus1d.hybrid.EMotor fields
  T_max_Nm = 200.0
  P_max_W = 60.0e3
  eta_drive = 0.92
  eta_regen = 0.85

  [powertrain.battery]    # aeolus1d.hybrid.Battery fields
  capacity_Ah = 50.0
  SoC = 0.6               # initial state-of-charge
  R_internal_Ohm = 0.1

  [powertrain.controller] # aeolus1d.hybrid.HybridController fields
  strategy = "load_following"   # or "fixed_fraction" / "engine_only" / "motor_only"
  motor_fraction = 0.3          # split used by the "fixed_fraction" strategy
  engine_min_T_Nm = 30.0
  regen_enabled = true

The four split strategies are load_following (default — the engine covers demand above engine_min_T_Nm, the motor trims the rest), fixed_fraction (the motor always takes motor_fraction of the demand), engine_only, and motor_only. Unknown keys in any of the three [powertrain.*] sub-tables — or an unknown sub-table name — raise.

Electrified drive cycle 'urban_illustrative': 150 s, dt=0.1 s
  distance         :    0.785 km
  tracking RMSE    :    0.189 m/s
  SoC              :   60.0% -> 59.8%
  electrical used  :    0.058 kWh (regen 0.029 kWh)
  engine fuel      :    1.087 L/100km
  tailpipe CO₂     :    25.02 g/km

The vehicle longitudinal dynamics are identical to the conventional run (it moves on the total engine + motor torque); the hybrid only allocates the source, so the difference is the energy split. The battery is a coulomb-counted SoC + OCV-map + internal-resistance model; drive discharges it, braking regenerates (clamped to [0, 1]). The deeper electrochemical battery tier (cell design / aging / pack thermal) is out of scope — import it as an FMU. See examples/86_electrified_drive_cycle.toml.

Programmatic use

The same machinery is a small Python API:

from aeolus1d.io.vehicle_case import load_vehicle_case, run_vehicle_case

case = load_vehicle_case("examples/85_vehicle_drive_cycle.toml")
trace, kpi = run_vehicle_case(case)
print(kpi.distance_km, kpi.fuel_l_per_100km)

A case that carries a [powertrain] block sets case.hybrid; run it through run_electrified_case(case) instead (it returns (trace, ElectrifiedKPIs) — SoC, electrical/regen energy, and the engine-share fuel + CO₂). The aeolus1d drive-cycle CLI auto-selects between the two by whether case.hybrid is None.

The underlying blocks — aeolus1d.vehicle, aeolus1d.driver, aeolus1d.drive_cycles, aeolus1d.driveline, aeolus1d.transmission_shift, aeolus1d.hybrid, aeolus1d.analysis.vehicle_tasks, and the aeolus1d.control.signal_toolbox — are all usable independently for acceleration / elasticity / gradeability / coastdown analysis tasks and for assembling custom controllers.