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:

  1. filter_vehicles — restrict the dwell data to the vehicle cohort selected for the current scenario.

  2. calc_vehicle_ranges — assign each vehicle a design range (miles) and battery capacity (kWh) derived from its observed shift mileage distribution.

  3. calc_dwell_durations — shrink each dwell window by plug-in/plug-out overhead and compute net available charging time.

  4. prepare_modes / assign_modes / build_mode_power_lut — annotate each dwell with the charging modes available at that location and the maximum deliverable power.

  5. merge_dwellset_node — join per-vehicle parameters (e.g. consumption rate) into the dwell table.

  6. calc_energy_use — compute per-trip energy demand (kWh).

  7. mark_critical_days — classify vehicle-days where en-route charging is necessary because total shift energy exceeds battery capacity.

  8. filter_dwells — drop en-route dwells that are too short or on non-critical days (pre-simulation filter).

  9. 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.

  10. simulate_charging_choice — run the forward-looking utility- maximisation charging-choice algorithm (Numba JIT-compiled) for every vehicle.

  11. 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 DwellSet objects. 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_choice is 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:

  1. 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/False or a dict specifying which loc_groups values enable the mode (with an optional invert_selection flag).

  2. 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 (see build_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 by prepare_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 in modes holding mode names.

    • loc_based_mode_avail (dict): Maps mode name → selector. Each selector is either a bool (applies globally) or a dict with loc_groups (list of location-group values that enable the mode) and optionally invert_selection (bool).

    • loc_group_col (str): Column in dw.data containing the location-group identifier for each dwell.

    • veh_based_mode_avail (dict): Configuration for the one vehicle-home-based mode, with sub-keys mode_name (str), ratio_col (str), and ratio_thresh (float).

    • mode_mask_col (str): Output column name for the integer mode-availability bitmask.

    • max_power_source_col (str): Column in modes holding 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 DwellSet with 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 modes already exists as a column in dw.data, if a mode specified in loc_based_mode_avail is not present in modes, if a selector value is neither bool nor dict, 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 via bool_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_modes so 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 with mode_names by position.

Return type:

dict[int, float]

Returns:

A dict mapping 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.start and dw.end are 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 in dw.data that hold the overhead durations (in the unit given by in_out_time_unit).

    • in_out_time_unit (str): Time unit string recognised by pd.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 DwellSet with adjusted dw.start / dw.end timestamps 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 in dw.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 in dw.data holding each vehicle’s energy consumption rate (kWh/mile).

Return type:

DwellSet

Returns:

The updated DwellSet with 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:

  1. Death range — the no_death_shift_frac quantile 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.

  2. Charge range — the no_charge_shift_frac quantile of each vehicle’s total-shift mileage, divided by the usable SoC band (soc_buffer_highsoc_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 (via pd.cut with 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 from params). 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 in dw.data

      • range_mi — output design-range column name in vehs

      • batt_kwh — output battery-capacity column name in vehs

      • consump_kwh_per_mi — energy-consumption-rate column in vehs

    • 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 vehs DataFrame 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_pandas in simulate_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.DataFrame with drop_cols removed.

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_days is 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 when filter_critical_days is True. 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 DwellSet with 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), AND

  • The simulated charge amount is zero.

Non-optional (real) dwells are always kept regardless of charge amount.

When filter_unused_optionals is False, 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 to False to retain all optional stops (e.g. for debugging).

    • filter_cols (dict): Required when filter_unused_optionals is True. 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 DwellSet with 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 in vehs that 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 in dw whose 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-backed dw after filtering.

Return type:

DwellSet

Returns:

The filtered DwellSet containing only dwells for vehicles found in vehs.

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:

  1. 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.

  2. 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.

  3. 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 DwellSet with a new boolean crit_col column and the intermediate crit_bound_col column 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:

  1. For dwells that are not refresh or critical, set their available power to 0 (they will be auto-skipped by the charging algorithm).

  2. Reverse-accumulate the max power with a CumAggFunc.MAX within each refresh segment, writing the result to every dwell in the segment (write_all=True).

  3. At refresh dwells themselves, overwrite the accumulated value with a sentinel (params["final_value"]), since at a refresh stop the remaining power budget resets.

  4. 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 DwellSet with 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_node that extracts the underlying DataFrame from dw, performs the merge, and writes the result back. All merge semantics (join type, key columns, etc.) are controlled by params exactly as they would be for a plain DataFrame merge node.

Parameters:
  • dw (DwellSet) – Dwell dataset whose data attribute 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 to merge_dataframes_node. Refer to that helper for the full parameter schema.

Return type:

DwellSet

Returns:

The updated DwellSet with dw.data replaced by the merged result.

laurel.pipelines.electrify_trips.nodes.prepare_modes(modes)[source]

Convert the charging-modes parameter dict into a tidy DataFrame.

The modes parameter dict maps mode names to their attribute dicts (e.g. maximum power). Two special keys — name_column and id_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, a mode_name column ("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 keys name_column and id_column; all remaining keys are treated as mode names whose values are attribute dicts.

Return type:

DataFrame

Returns:

A pd.DataFrame with 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 ForwardLookingChargingChoiceStrategy iterates 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 by prepare_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 to ForwardLookingChargingChoiceStrategy.__init__. Must include modes_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 from dw.data after the simulation (e.g. intermediate look-ahead columns).

Return type:

DwellSet

Returns:

The updated DwellSet with charging-decision columns added (charge amount, mode chosen, accumulated delay, etc.) and drop_cols removed.

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.

Sub-pipelines / tags

  • choose_charging — the single charge_pipe that runs the full electrification simulation for one State of the World: filters vehicles, assigns battery ranges, calculates energy requirements, marks critical days, and runs the utility-maximisation charging-choice algorithm. Outputs are written to a scenario-keyed partition in data/07_model_output/.

To visualise the node graph interactively, run:

kedro viz run

then open http://localhost:4141 in a browser and select electrify_trips from the pipeline dropdown.

laurel.pipelines.electrify_trips.pipeline.create_pipeline(**kwargs)[source]
Return type:

Pipeline

Module contents

This is a boilerplate pipeline ‘electrify_trips’ generated using Kedro 0.19.1

laurel.pipelines.electrify_trips.create_pipeline(**kwargs)[source]
Return type:

Pipeline