laurel.pipelines.electrify_trips package
Submodules
laurel.pipelines.electrify_trips.nodes module
Kedro pipeline nodes for the electrify_trips pipeline (Model Module 4).
This module implements the per-vehicle charging-choice simulation described in Module 4 (“Simulate electrified dwells”) of Passow & Rajagopal (2026). Starting from a dataset of observed diesel-HDT dwell events, it produces a dataset of electrified dwells annotated with charging decisions — which mode was used, how much energy was transferred, and how much delay was incurred — for a single scenario (state of the world).
Pipeline overview
The nodes in this module are executed in the following logical order by the Kedro pipeline:
filter_vehicles — restrict the dwell data to the vehicle cohort selected for the current scenario.
calc_vehicle_ranges — assign each vehicle a design range (miles) and battery capacity (kWh) derived from its observed shift mileage distribution.
calc_dwell_durations — shrink each dwell window by plug-in/plug-out overhead and compute net available charging time.
prepare_modes / assign_modes / build_mode_power_lut — annotate each dwell with the charging modes available at that location and the maximum deliverable power.
merge_dwellset_node — join per-vehicle parameters (e.g. consumption rate) into the dwell table.
calc_energy_use — compute per-trip energy demand (kWh).
mark_critical_days — classify vehicle-days where en-route charging is necessary because total shift energy exceeds battery capacity.
filter_dwells — drop en-route dwells that are too short or on non-critical days (pre-simulation filter).
mark_shift_powers — record the maximum power available later in the shift at each dwell, used as a look-ahead input to the charging algorithm.
simulate_charging_choice — run the forward-looking utility- maximisation charging-choice algorithm (Numba JIT-compiled) for every vehicle.
filter_dwells_post — drop optional stops where the simulation determined no charging occurred (post-simulation filter).
Key design decisions
Critical-day filtering: En-route (truck-stop) charging is only considered on vehicle-days where the total shift energy demand exceeds battery capacity. This reduces the search space and avoids over-estimating public-charging demand on days when depot charging alone suffices.
Optional stops: Proxy dwells inserted at truck stops along routed paths have zero net duration. They are carried through the pre-simulation filter and dropped post-simulation if the vehicle chose not to charge there.
Dask support: All nodes handle both pandas- and Dask-backed
DwellSetobjects. Dask execution partitions the vehicle fleet so that each partition is processed independently; callers must ensure vehicle sequences are not split across partitions.Numba JIT compilation: The inner charging loop in
simulate_charging_choiceis JIT-compiled. An optional pre-compilation step warms up the compiled functions before the main run.
References
Passow, F., & Rajagopal, R. (2026). Identifying indicators to inform proactive substation upgrades for charging electric heavy-duty trucks. Applied Energy (submitted March 2026).
Liu, J., et al. Utility-maximisation charging choice model (inspiration for
ForwardLookingChargingChoiceStrategy).
- laurel.pipelines.electrify_trips.nodes.assign_modes(dw, modes, params)[source]
Annotate each dwell with available charging modes and maximum deliverable power.
Charging-mode availability at a dwell is determined by two independent mechanisms and then encoded into a compact integer bitmask:
Location-based modes (e.g. truck-stop charging): A boolean column is created for each mode whose availability depends on the dwell’s location group. The selector can be a literal
True/Falseor a dict specifying whichloc_groupsvalues enable the mode (with an optionalinvert_selectionflag).Vehicle-based depot mode: A single mode whose availability depends on whether the vehicle’s home-base ratio exceeds a threshold. The ratio column is supplied via
params["veh_based_mode_avail"].
After building one boolean column per mode, all boolean columns are combined into an integer bitmask (via
bool_arr_to_bits), and the maximum power deliverable under each possible bitmask combination is looked up from a pre-built LUT (seebuild_mode_power_lut). The individual boolean mode columns are then dropped.- Parameters:
dw (
DwellSet) – Dwell dataset to annotate. Modified in-place and returned.modes (
DataFrame) – Modes table produced byprepare_modes, with one row per charging mode and columns for mode name and maximum power.params (
dict) –Pipeline parameters. Expected keys:
mode_col(str): Column inmodesholding mode names.loc_based_mode_avail(dict): Maps mode name → selector. Each selector is either abool(applies globally) or a dict withloc_groups(list of location-group values that enable the mode) and optionallyinvert_selection(bool).loc_group_col(str): Column indw.datacontaining the location-group identifier for each dwell.veh_based_mode_avail(dict): Configuration for the one vehicle-home-based mode, with sub-keysmode_name(str),ratio_col(str), andratio_thresh(float).mode_mask_col(str): Output column name for the integer mode-availability bitmask.max_power_source_col(str): Column inmodesholding each mode’s maximum power (kW).max_power_col(str): Output column name for the maximum deliverable power at each dwell.
- Return type:
DwellSet- Returns:
The annotated
DwellSetwith a mode-availability bitmask column and a maximum-power column added; individual per-mode boolean columns are removed.- Raises:
ValueError – If any mode name in
modesalready exists as a column indw.data, if a mode specified inloc_based_mode_availis not present inmodes, if a selector value is neitherboolnordict, or if any mode remains unassigned after processing both location- and vehicle-based rules.
- laurel.pipelines.electrify_trips.nodes.build_mode_power_lut(mode_names, mode_powers)[source]
Pre-compute the maximum deliverable power for every mode-availability bitmask.
Enumerates all 2^N combinations of N charging modes (where N is
len(mode_names)), encodes each combination as an integer bitmask viabool_arr_to_bits, and records the maximum mode power across all enabled modes in that combination. A bitmask of zero (no modes available) maps to 0.0 kW.This lookup table is used by
assign_modesso that per-dwell maximum power can be obtained by a single O(1) dictionary lookup rather than recomputing the max over enabled modes for every row.- Parameters:
mode_names (
Series) – Ordered series of charging-mode name strings. The order determines bit positions in the bitmask (first name → least significant bit).mode_powers (
Series) – Series of maximum power values (kW), aligned withmode_namesby position.
- Return type:
dict[int,float]- Returns:
A
dictmapping each integer bitmask (int) to the maximum deliverable power (float, kW) across all modes enabled in that bitmask.
- laurel.pipelines.electrify_trips.nodes.calc_dwell_durations(dw, params)[source]
Compute net charging-available dwell duration for each stop.
Converts raw plug-in and plug-out overhead times from a numeric unit to
timedelta, then shrinks each dwell’s start and end times inward by those overhead amounts. The remaining window represents the time actually available for charging. Optional stops — identified by identical start and end times — are left with zero duration so they are not double-counted.The resulting net dwell duration (in hours, as a float) is written to a new column.
- Parameters:
dw (
DwellSet) – Dwell dataset.dw.startanddw.endare timestamp columns that bound each dwell. Modified in-place and returned.params (
dict) –Pipeline parameters. Expected keys:
in_out_time_cols(dict): Maps"plug_in"and"plug_out"to the column names indw.datathat hold the overhead durations (in the unit given byin_out_time_unit).in_out_time_unit(str): Time unit string recognised bypd.to_timedelta/dd.to_timedelta(e.g."s"for seconds,"min"for minutes).dwell_time_col(str): Name of the output column that will hold the net dwell duration in hours.
- Return type:
DwellSet- Returns:
The updated
DwellSetwith adjusteddw.start/dw.endtimestamps and a new float column for net dwell hours.
- laurel.pipelines.electrify_trips.nodes.calc_energy_use(dw, params)[source]
Compute per-trip energy demand (kWh) as trip distance × consumption rate.
Multiplies the trip-distance column (
dw.trip_dist) by a vehicle-specific energy-consumption rate column to produce per-trip energy demand in kWh. Both columns must already be present indw.data; the result is written to a new column.- Parameters:
dw (
DwellSet) – Dwell dataset containing trip distances and energy-consumption rates. Modified in-place and returned.params (
dict) –Pipeline parameters. Expected keys:
energy_col(str): Name for the output energy column (kWh).consump_col(str): Name of the column indw.dataholding each vehicle’s energy consumption rate (kWh/mile).
- Return type:
DwellSet- Returns:
The updated
DwellSetwith the new energy column appended.
- laurel.pipelines.electrify_trips.nodes.calc_vehicle_ranges(vehs, dw, params)[source]
Assign a design range (miles) and battery capacity (kWh) to each vehicle.
Design range is determined by taking the maximum of two per-vehicle criteria computed from observed shift mileage distributions:
Death range — the
no_death_shift_fracquantile of each vehicle’s single-shift longest-trip distances. The vehicle must be able to complete any shift’s longest leg without running out of charge.Charge range — the
no_charge_shift_fracquantile of each vehicle’s total-shift mileage, divided by the usable SoC band (soc_buffer_high−soc_buffer_low). The vehicle should be able to complete a typical full shift on one charge.
The continuous desired range is then rounded up to the nearest value in
range_options_miles(viapd.cutwith the top bin open), and multiplied by the vehicle’s energy-consumption rate to obtain battery capacity.- Parameters:
vehs (
DataFrame) – Vehicle-level table to augment. Two columns are added in-place: the design range column and the battery capacity column (names taken fromparams). The vehicle index is used to join shift statistics back to vehicles.dw (
DwellSet) – Dwell dataset containing trip-distance and shift-ID columns used to derive per-vehicle shift mileage statistics.params (
dict) –Pipeline parameters. Expected keys:
columns(dict): Column-name mappings with sub-keys:shift— shift identifier column indw.datarange_mi— output design-range column name invehsbatt_kwh— output battery-capacity column name invehsconsump_kwh_per_mi— energy-consumption-rate column invehs
no_death_shift_frac(float): Quantile (0–1) of the longest-trip distribution used for the death-range criterion.no_charge_shift_frac(float): Quantile (0–1) of the total-shift-miles distribution used for the charge-range criterion.soc_buffer_high(float): Upper SoC target (fraction, 0–1).soc_buffer_low(float): Lower SoC buffer (fraction, 0–1).range_options_miles(list[float]): Ordered list of candidate design ranges in miles (e.g.[150, 300, 500]). The top entry acts as the ceiling; everything above the second-to-last bin edge is mapped to the top option.
- Returns:
design range (miles) and battery capacity (kWh).
- Return type:
The
vehsDataFrame with two new columns appended
- laurel.pipelines.electrify_trips.nodes.drop_cols_to_pandas(vehs, params)[source]
Drop columns unneeded downstream and coerce the result to a plain pandas DataFrame.
Geometry-bearing GeoDataFrames (e.g. from
vehicles_labelled) carry columns that Dask cannot pyarrow-string-encode (raw WKB/shapely objects). This node drops those columns and returns a plain DataFrame so that later Dask operations (e.g.dd.from_pandasinsimulate_charging_choice) never see them.- Parameters:
vehs (pd.DataFrame | gpd.GeoDataFrame) – DataFrame or GeoDataFrame to process.
params (dict) –
Configuration dict with the following key:
drop_cols(list[str]): Column names to drop.
- Return type:
pd.DataFrame
- Returns:
Plain
pd.DataFramewithdrop_colsremoved.
- laurel.pipelines.electrify_trips.nodes.filter_dwells(dw, params)[source]
Drop en-route dwells that cannot meaningfully contribute to charging.
Applied before the charging-choice simulation. A dwell is dropped when:
Its net available duration is negative (shorter than plug-in + plug-out overhead), OR
(When
filter_critical_daysis enabled) it is neither a refresh location nor part of a critical day, AND it is not an optional stop.
Keeping optional stops (zero-duration proxy stops inserted along routes) regardless of the critical-day flag preserves them for the post-simulation filter, which uses actual charging decisions to decide whether they were visited.
Dropped rows are handled via
_filter_dwells_core, which accumulates trip-distance and related columns through the mask before removing rows so that successive kept rows maintain correct running totals.- Parameters:
dw (
DwellSet) – Dwell dataset to filter. Modified in-place and returned.params (
dict) –Pipeline parameters. Expected keys:
dwell_time_col(str): Column holding net dwell duration in hours (negative means too short to plug in/out).filter_critical_days(bool): Whether to apply the critical-day filter in addition to the duration filter.filter_cols(dict): Required whenfilter_critical_daysisTrue. Sub-keys:refresh— boolean column marking refresh-eligible dwells.crit— boolean column marking critical-day dwells.
accum_cols_forward_extra(list[str]): Extra columns to accumulate in the forward direction through the mask.accum_cols_reverse(list[str]): Columns to accumulate in the reverse direction through the mask.drop_cols(list[str]): Additional columns to drop after filtering.
- Return type:
DwellSet- Returns:
The filtered
DwellSetwith adjusted cumulative columns and reduced row count.
- laurel.pipelines.electrify_trips.nodes.filter_dwells_post(dw, params)[source]
Drop optional stops where the vehicle chose not to charge.
Applied after the charging-choice simulation. Optional stops are proxy dwells with zero net duration that were inserted at potential en-route charging locations (e.g. truck stops along a routed path). If the simulation determined that no charging occurs at an optional stop, that row carries no useful information and is removed to keep the output dataset compact.
A dwell is dropped when both conditions hold:
It is an optional stop (
dw.end <= dw.start), ANDThe simulated charge amount is zero.
Non-optional (real) dwells are always kept regardless of charge amount.
When
filter_unused_optionalsisFalse, this function is a no-op.- Parameters:
dw (
DwellSet) – Dwell dataset after charging simulation, containing charge-amount and timing columns. Modified in-place and returned.params (
dict) –Pipeline parameters. Expected keys:
filter_unused_optionals(bool): Whether to apply the filter. Set toFalseto retain all optional stops (e.g. for debugging).filter_cols(dict): Required whenfilter_unused_optionalsisTrue. Sub-key:charge— column holding the simulated charge amount (kWh) for each dwell.
accum_cols_forward_extra(list[str]): Extra columns to accumulate in the forward direction through the mask.accum_cols_reverse(list[str]): Columns to accumulate in the reverse direction through the mask.drop_cols(list[str]): Additional columns to drop after filtering.
- Return type:
DwellSet- Returns:
The filtered
DwellSetwith unused optional stops removed and cumulative columns corrected.
- laurel.pipelines.electrify_trips.nodes.filter_vehicles(dw, vehs, params)[source]
Filter the dwell data to only include vehicles present in the vehicles table.
Retains only dwell rows whose vehicle index appears in
vehs. Any vehicle invehsthat has no dwell rows generates a warning. For Dask-backed DwellSets the filtered result is repartitioned to a fixed number of partitions to rebalance work after dropping rows.- Parameters:
dw (
DwellSet) – The dwell dataset to filter. Modified in-place and returned.vehs (
DataFrame) – Vehicle-level table whose index contains the set of vehicle IDs to keep. Rows indwwhose vehicle ID is absent from this index are dropped.params (
dict) –Pipeline parameters. Expected keys:
n_partitions(int): Target partition count used when repartitioning a Dask-backeddwafter filtering.
- Return type:
DwellSet- Returns:
The filtered
DwellSetcontaining only dwells for vehicles found invehs.
- laurel.pipelines.electrify_trips.nodes.mark_critical_days(dw, params)[source]
Classify each dwell as belonging to a critical or non-critical vehicle-day.
A vehicle-day is critical when the total energy demand for all remaining trips in the shift exceeds the vehicle’s battery capacity, making en-route charging necessary. Non-critical days can be completed on a single depot/destination charge, so en-route dwells on those days are candidates for filtering.
The algorithm proceeds in three steps:
Identify refresh boundaries — a refresh dwell is one that has sufficient dwell duration to fully recharge the battery (duration ≥ battery capacity / max power). These mark the segment boundaries within which energy demand is accumulated.
Accumulate remaining-shift energy — starting from each refresh boundary and working backwards, the energy demand of subsequent trips is summed up to the next refresh boundary. This gives the energy a vehicle would need on-board if it arrived at the refresh stop empty.
Classify and propagate — a dwell is initially critical if its accumulated remaining-shift energy exceeds battery capacity. The critical flag is then forward-filled within each vehicle’s sequence so that all dwells between a refresh boundary and the critical trip inherit the flag. Partial days (no prior refresh boundary) are conservatively treated as critical.
- Parameters:
dw (
DwellSet) – Dwell dataset with energy, duration, power, battery-capacity, and refresh columns already populated. Modified in-place and returned.params (
dict) –Pipeline parameters. Expected keys:
refresh_col(str): Boolean column marking refresh-eligible dwells (e.g. truck-stop dwells with sufficient dwell time).crit_bound_col(str): Temporary column name used internally for the refresh-and-can-charge boundary flag.batt_cap_col(str): Column holding each dwell row’s vehicle battery capacity (kWh).max_power_col(str): Column holding maximum available charging power at each dwell (kW).dur_col(str): Column holding net dwell duration (hours).energy_col(str): Column holding per-trip energy demand (kWh).energy_col_next_trip(str): Temporary column for the next trip’s energy demand.energy_col_remain_shift(str): Output column for total remaining-shift energy demand (kWh).crit_col(str): Output boolean column marking critical-day dwells.
- Return type:
DwellSet- Returns:
The updated
DwellSetwith a new booleancrit_colcolumn and the intermediatecrit_bound_colcolumn removed.
- laurel.pipelines.electrify_trips.nodes.mark_shift_powers(dw, params)[source]
Record the maximum charging power available later in the shift at each dwell.
The forward-looking charging-choice algorithm needs to know whether a vehicle will have access to a high-power charger later in the shift before it decides whether to charge now. This node pre-computes that look-ahead value.
The approach:
For dwells that are not refresh or critical, set their available power to 0 (they will be auto-skipped by the charging algorithm).
Reverse-accumulate the max power with a
CumAggFunc.MAXwithin each refresh segment, writing the result to every dwell in the segment (write_all=True).At refresh dwells themselves, overwrite the accumulated value with a sentinel (
params["final_value"]), since at a refresh stop the remaining power budget resets.Shift the accumulated column backward by one position so each dwell sees the future maximum, not its own value.
- Parameters:
dw (
DwellSet) – Dwell dataset with refresh, critical, and max-power columns already populated. Modified in-place and returned.params (
dict) –Pipeline parameters. Expected keys:
refresh_col(str): Boolean column marking refresh-eligible dwells.crit_col(str): Boolean column marking critical-day dwells.max_power_col(str): Column holding per-dwell maximum charging power (kW).final_value(float): Sentinel power value assigned to refresh dwells after accumulation (typically the maximum possible charger power, kW).fill_value(float): Fill value used for the shift at the end of a vehicle’s sequence (typically 0.0).max_power_col_shift(str): Output column name for the shifted look-ahead power (kW).
- Return type:
DwellSet- Returns:
The updated
DwellSetwith a new look-ahead power column and intermediate columns removed.
- laurel.pipelines.electrify_trips.nodes.merge_dwellset_node(dw, right, params)[source]
Merge a DataFrame into a DwellSet using the standard merge-node helper.
A thin wrapper around
merge_dataframes_nodethat extracts the underlying DataFrame fromdw, performs the merge, and writes the result back. All merge semantics (join type, key columns, etc.) are controlled byparamsexactly as they would be for a plain DataFrame merge node.- Parameters:
dw (
DwellSet) – Dwell dataset whosedataattribute is used as the left side of the merge. Modified in-place and returned.right (
DataFrame) – DataFrame to merge in on the right side.params (
dict) – Merge parameters forwarded verbatim tomerge_dataframes_node. Refer to that helper for the full parameter schema.
- Return type:
DwellSet- Returns:
The updated
DwellSetwithdw.datareplaced by the merged result.
- laurel.pipelines.electrify_trips.nodes.prepare_modes(modes)[source]
Convert the charging-modes parameter dict into a tidy DataFrame.
The
modesparameter dict maps mode names to their attribute dicts (e.g. maximum power). Two special keys —name_columnandid_column— control the column and index names of the resulting table and are removed before conversion.Example input (YAML):
modes: name_column: mode_name id_column: mode_id depot: max_power_kw: 150 truck_stop: max_power_kw: 350
Produces a DataFrame with index name
mode_id, amode_namecolumn ("depot","truck_stop", …), and one column per attribute.- Parameters:
modes (
dict) – Charging-modes parameter dictionary, typically loaded from the Kedro parameters YAML. Must contain the two special keysname_columnandid_column; all remaining keys are treated as mode names whose values are attribute dicts.- Return type:
DataFrame- Returns:
A
pd.DataFramewith one row per charging mode, indexed by a sequential integer ID, and columns for the mode name and each mode attribute.
- laurel.pipelines.electrify_trips.nodes.simulate_charging_choice(dw, vehs, modes, params)[source]
Run the forward-looking charging-choice simulation for all vehicles.
For each vehicle, the
ForwardLookingChargingChoiceStrategyiterates through dwells in chronological order and selects — at each stop — the charging mode and energy amount that maximises a utility function while respecting battery, power, and delay constraints. The strategy is inspired by Liu et al. and is implemented with Numba JIT compilation for speed.Supports both pandas (single-process) and Dask (distributed) backends. For Dask, the simulation runs independently on each partition; it is the caller’s responsibility to ensure that each partition contains complete, sorted vehicle sequences (no vehicle spans multiple partitions).
Optionally pre-compiles the Numba JIT functions using a small mock dataset before the main run. Pre-compilation is useful for single-process runs but does not help distributed Dask workers (each worker JIT-compiles on first use).
- Parameters:
dw (
DwellSet) – Dwell dataset sorted by vehicle and time. For pandas-backed DwellSets, sorting is performed automatically; for Dask-backed DwellSets, the caller must guarantee sort order is preserved across partitions.vehs (
DataFrame) – Vehicle-level table with battery capacity and other per-vehicle parameters required by the charging-choice strategy.modes (
DataFrame) – Charging-modes table produced byprepare_modes, with one row per mode and columns for power limits and other attributes.params (
dict) –Pipeline parameters. Expected keys:
input_cols(dict): Column-name mappings forwarded toForwardLookingChargingChoiceStrategy.__init__. Must includemodes_avail(bitmask column name) among others.precompile(bool): Whether to trigger Numba pre-compilation before the main simulation run.drop_cols(list[str]): Columns to drop fromdw.dataafter the simulation (e.g. intermediate look-ahead columns).
- Return type:
DwellSet- Returns:
The updated
DwellSetwith charging-decision columns added (charge amount, mode chosen, accumulated delay, etc.) anddrop_colsremoved.
laurel.pipelines.electrify_trips.pipeline module
Kedro pipeline definition for the electrify_trips pipeline.
Wires the nodes from laurel.pipelines.electrify_trips.nodes into a single Pipeline object.
For full documentation of each node’s inputs, outputs, and algorithm,
see laurel.pipelines.electrify_trips.nodes.
Module contents
This is a boilerplate pipeline ‘electrify_trips’ generated using Kedro 0.19.1