laurel.models package
Submodules
laurel.models.charging_algorithms module
Charging-choice simulation strategies for the electrify_trips pipeline (Model Module 4).
Implements the per-vehicle utility-maximisation charging-choice model described
in the paper. Each concrete AbstractChargingChoiceStrategy encodes a
different decision rule for when and how much to charge at each dwell, and
is invoked by AbstractChargingChoiceStrategy.run() which iterates over
all vehicles in a DwellSet.
Pipeline overview
AbstractChargingChoiceStrategy.run()— outer loop: convertsDwellSet.data, vehicle parameters, and mode tables to NumPy recarrays; dispatches per-vehicle to_simulate().AbstractChargingChoiceStrategy._simulate()— JIT-compiled vehicle loop: evolves SoC step-by-step, calling_choose_charging()at each eligible dwell.AbstractChargingChoiceStrategy._choose_charging()— abstract static method; each concrete strategy overrides this with a@jit-decorated function expressing its decision logic.
Concrete strategies
SoCThreshChargingChoiceStrategy: charges when SoC falls below a threshold; simple rule used for baseline and validation runs.ForwardLookingChargingChoiceStrategy: evaluates six charging-energy options × available modes, scores each by an indirect utility function (SoC target + delay cost + feasibility penalties), and selects the maximum.
Key design decisions
Recarray-based Numba interface: all simulation inputs are converted to NumPy structured arrays (recarrays) before being passed to JIT functions. Column names are mapped to recarray field names via
_renamer(a reversed{column_name → attribute_name}dict). Pandas nullable types (Float64,Int64) and booleans are substituted by_replace_dtypesto avoid Numba incompatibility.Bitmask mode availability: available charging modes per dwell are encoded as a
uint64bitmask in themodes_availfield and decoded inside the JIT function viabits_to_bool_vec(). This avoids passing variable-length arrays across the Python/Numba boundary.``_output_records_dtype``: a fixed structured dtype shared by all strategies ensures that the output recarray can always be concatenated with the input DwellSet data without column-name conflicts.
Delay accounting:
delay_inc_hrsanddelay_dec_hrstrack separately the delay incurred and recovered at each dwell;cur_delayis the running balance. Themax_delay_recoverable_hrsvehicle parameter caps how much accumulated delay can be erased at a single refresh point (depot or destination stop).Revive logic: a vehicle whose SoC goes negative is treated as “broken down” and neither charges nor accumulates delay until it reaches the next
refreshdwell (a depot or destination stop with sufficient dwell time), where it is revived with SoC = 0.
References
Passow, F., & Rajagopal, R. (2026). Identifying indicators to inform proactive substation upgrades for charging electric heavy-duty trucks. Applied Energy.
Liu, Y., et al. (2021). A hierarchical optimization charging strategy for plug-in hybrid electric vehicles. IEEE Transactions on Vehicular Technology.
laurel.models.dwell_sets module
Central data structure for vehicle dwell histories and dwell-level operations.
DwellSet wraps a pandas or Dask DataFrame in which each row represents a
single dwell (parking stop) for a heavy-duty truck. It enforces a canonical
sort order (vehicle, dwell-start time), manages a set of named semantic columns
(vehicle ID, hex cell, start/end times, trip distance/duration, reset flag), and
provides the core operations that downstream pipelines build on:
Masked accumulation (
accum_masked()): forward or reverse cumulative aggregation of trip-distance/duration across consecutive dwells that will be removed, respectingresetboundaries. Used to propagate consumed-energy estimates from inserted optional stops back to their flanking depots.Masked reset propagation (
reset_masked()): ensures that the first retained dwell after a gap introduced by masking hasreset=True, so that the charging-choice simulator restarts the SoC correctly.Dwell → event conversion (
to_events()): reshapes from one-dwell-per-row to one-event-per-row for load profile construction.
Module-level helper functions load_dwell_set() and save_dwell_set()
provide the Kedro I/O interface.
Key design decisions
Vehicle ID as DataFrame index: The vehicle-ID column is stored as the DataFrame index rather than a regular column. This lets Dask partition by vehicle so that per-vehicle groupby operations never need cross-partition communication.
Numba JIT inner loops: The accumulation core (
_accum_masked_core()) and reset core (_reset_masked_grp_core()) are decorated with@njit. NumPy structured-array dtypes that are incompatible with Numba (booleans, nullable integers) are substituted via_replace_dtypesbefore the recarray is created.``reset`` column semantics: a
Truevalue at rowimeans “this dwell begins a new simulation epoch” — the charging simulator resets the vehicle’s SoC to full at the start of each epoch. The default behaviour (when noresetcolumn is supplied) marks only the first dwell of each vehicle as a reset.``CumAggFunc`` enum: accumulation supports SUM, PRODUCT, MAX, and MIN so the same
accum_maskedmethod can propagate both additive quantities (trip distance, trip duration) and multiplicative ones.
References
Passow, F., & Rajagopal, R. (2026). Identifying indicators to inform proactive substation upgrades for charging electric heavy-duty trucks. Applied Energy.
laurel.models.group_times module
Time-grouping classes for aggregating load profiles and normalising observation counts.
Provides AbstractTimeGrouper and three concrete implementations used
to group dwell events and count how many observations are possible within each
time group across the study period. The “possible observation count” is used
by NonzeroGroupedSummarizer to correctly
include zeros in quantile calculations.
Concrete groupers
HourOfWeekdayGrouper: groups by (is_weekend × local hour of day).LocalHourOfDayGrouper: groups by local hour of day only.AdaptiveTimeGrouper: infers the minimum set of time attributes needed to uniquely identify each bin within the study window (e.g. year, day-of-week, hour), adapting to arbitraryfreqstrings.
Key design decisions
Possible count computation:
AbstractTimeGrouper.get_possible_obs_counts()builds a full UTC time range between the study start and end, converts to each possible timezone inpossible_tzs, appliesadd_group_classes, and counts how many UTC timestamps fall in each (timezone × group) bin. This correctly handles DST transitions and unequal bin sizes.Timezone handling: all internal timestamps are UTC; local-time conversion is applied only to compute group labels, not stored in the data. This avoids ambiguous or non-existent local times at DST boundaries.
``WEEKEND_FIRST_DAY = 5``: ISO weekday numbering (Monday = 0); day 5 is Saturday, so
day_of_week >= 5selects Saturday and Sunday.
laurel.models.manage_charging module
Charging management classes that convert dwell records to load-profile events.
After the charging-choice simulation (laurel.models.charging_algorithms)
determines how much energy each vehicle charges at each dwell, the charging
managers in this module determine when that energy flows. They translate
per-dwell charging assignments into a sequence of timestamped power events
that can be assembled into load profiles.
The class hierarchy is:
AbstractChargingManager— stores column bindings and the abstractget_events()interface.IndependentDwellChargingManager— handles the common case where each dwell is managed independently (no cross-dwell optimisation). Defines theseq_namespattern and the dwell-to-event pipeline.MinPowerChargingManager— spreads energy at the minimum constant power needed to deliver the required kWh over the full dwell duration.ImmediateChargingManager— charges at maximum available power as early as possible, computing the exact charge-end timestamp.
Key design decisions
``seq_names`` event structure: each concrete manager defines a list of named event-sequence prefixes (e.g.
["dwell_start", "dwell_end"]). For each sequence name, the manager adds columns{seq_name}_time,{seq_name}_duration,{seq_name}_power_kw, and{seq_name}_plugged. Theto_events()method then pivots from wide (one dwell per row) to long (one event per row) format.``ProfileType`` enum:
OBSERVATIONSmode writes absolute power at each event boundary (suitable for step-function integration);DIFFERENCESmode writes the change in power (positive at plug-in, negative at plug-out), which enables efficient sparse load profile construction via cumulative sum.Property renaming pattern: the
energy,duration,max_power,region,scale_up, andcostproperties delegate to_rename_idx_col(), so assigning a new column name transparently renames the underlying DataFrame column.``_MANAGER_MAP``: populated at module load time by introspecting
globals()for subclasses ofAbstractChargingManager. Allows pipeline nodes to instantiate managers by name without a static registry.
laurel.models.probability_localization module
Localised electrification probability estimation for the evaluate_impacts pipeline (Model Module 5).
Provides ElectProbLocalizer, which fuses two probability tables to
produce a spatially localised adoption probability
P(E | L, V) — the probability that a vehicle of class V operating
primarily in location class L will be electrified under a given scenario.
The two input tables are:
Structural table
P(n_electrified | n_obs, L, V): the observed ratio of electrified dwell visits to total visits for each (location class, vehicle class) cell in the telematics dataset.Target table
P(E | V): the scenario-level adoption rate for each vehicle operating-distance class, drawn from the Beta-distribution/Gaussian- copula samples generated by theprepare_totalspipeline.Weight table
P(L | V): the conditional probability that a vehicle of classVvisits location classL, estimated from the telematics data.
Algorithm (4 steps)
Fit a ridge-penalised binomial GLM to the observed proportions
n_electrified / n_obsfor cells withn_obs ≥ min_obs, using a Patsy formula overC(loc_col) + C(veh_col)(or a custom formula) with observation weights equal ton_obs.Predict linear predictors
η̂_{L,V}for all (L, V) cells, including those below the observation threshold that were excluded from fitting.Calibrate per-vehicle offsets
δ_Vby solvingΣ_L P(L|V) · σ(η̂_{L,V} + δ_V) = P(E|V)for each vehicle class using Brent’s method. If the root is not bracketed, the bracket is expanded geometrically up tomax_bracket_expansionstimes; if still not bracketed, falls back to a logit-difference approximation.Return the calibrated probabilities
P(E|L,V) = σ(η̂_{L,V} + δ_V)as a Series aligned with the input DataFrame.
Key design decisions
Ridge penalty (L2): the GLM may be poorly conditioned when some (L, V) cells have very few observations. Ridge regularisation stabilises the estimates without dropping cells, and the intercept is unpenalised by default so that the overall adoption level is unconstrained.
Per-vehicle calibration step: the GLM gives a relative distribution of adoption across location classes, but the absolute level must match the scenario target
P(E|V). The additive offsetδ_Vshifts the logit uniformly across all location classes for a given vehicle class, preserving relative rank ordering while exactly matching the marginal target.Brent’s method with bracket expansion:
brentqrequires a sign change in the objective. For extreme target values (near 0 or 1), the default bracket[−25, +25]on the logit scale may be insufficient; the expansion loop doubles the bracket up tomax_bracket_expansionstimes.
References
Passow, F., & Rajagopal, R. (2026). Identifying indicators to inform proactive substation upgrades for charging electric heavy-duty trucks. Applied Energy.
laurel.models.sampling module
Bootstrap load-profile assembly for the evaluate_impacts pipeline (Model Module 6).
Assembles per-substation peak-load and energy estimates by bootstrap sampling
across the telematics-observed dwell population for each hexagonal TAZ. The
central function is sample_profiles(), which performs a two-stage
inverse-propensity-weighted draw:
Stage 1 (class-level draw): For each hex cell, draw candidate dwells from the observed dwell population in the same freight-activity class, using normalised inverse-propensity weights
Om_class. This ensures that each hex’s sample is drawn from a class-representative pool even when the hex itself has few direct observations.Stage 2 (self draw): Supplement with dwells drawn directly from the hex’s own observed dwell population, up to the available count.
Supporting sparse-matrix utilities
build_entity_mask_array(): builds a(n_obs, n_ent)CSC indicator array mapping observations to entities.normalize_sparse(): column- or row-normalises a sparse array, with configurable zero-sum handling.sample_sparse_multinomial()/sample_sparse_multinomial_core(): JIT-compiled multinomial draw from each column of a sparse probability matrix.collate_sparse_diffs()/_collate_sparse_diffs_core(): converts sparse power-difference arrays to cumulative load-profile DataFrames (one region × event row per entry).calculate_value_time_units(),calculate_peak_units(),discretize_sparse_profiles(): summarise profiles into kWh totals, peak kW, and discretised time-series DataFrames.
Key design decisions
Sparse representation: each possible charging event is stored as a row in the event-observation matrix; the sparse format avoids materialising dense
(n_events × n_regions)arrays that would be prohibitively large for 52,000 substations × 100 bootstrap draws.Bernoulli fractional-sample rounding: expected dwell counts are non-integer; fractional parts are rounded stochastically via a Bernoulli draw (
Binomial(n=1, p=fractional_part)) to avoid systematic bias.JIT inner loop:
sample_sparse_multinomial_core()iterates over regions in a tight loop that would be slow in interpreted Python; the@jitdecorator eliminates the per-region Python overhead.
References
Passow, F., & Rajagopal, R. (2026). Identifying indicators to inform proactive substation upgrades for charging electric heavy-duty trucks. Applied Energy.
laurel.models.summarize module
Time-series spreading and sparse quantile summarisation utilities.
Provides two core classes used throughout the load-profile assembly pipeline:
IntervalBeginSpreader: given a DataFrame of events with start timestamps and durations, “spreads” each event onto everyfreq-aligned time-bin beginning that it covers. Used bydiscretize_sparse_profiles()to assign constant-power observations to each hourly bin they span.NonzeroGroupedSummarizer: computes quantiles of sparse data where many possible observations are zero but only the non-zero values are stored. Correctly pads with the appropriate number of zeros before quantile calculation. Used to compress many-draw bootstrap profiles to a small set of quantiles (e.g. 20th, 50th, 80th, 95th percentile) per (substation, hour) cell.
Key design decisions
Interval-begin convention: the spreader produces new rows at the start of each covered time bin (not the end). This is consistent with the step- function load-profile convention used throughout the pipeline: a constant power level
pstarts at timetand persists until the next event.UTC-only timestamps:
IntervalBeginSpreader.spread()requires the time column to be either timezone-naive or UTC; other timezones raise an error. Timezone-naive inputs are temporarily localised to UTC for the spreading arithmetic and then de-localised on output.``IndexIntegerizer`` for group labels: group columns may contain string or categorical labels that are expensive to compare. The spreader converts them to compact integer codes before passing to the JIT core and restores the original labels afterward.
Zero-padding in quantiles:
NonzeroGroupedSummarizeronly stores non-zero events; thepossible_count_coltells the summariser how many total observations exist (including zeros) so that quantiles correctly account for the zero-inflation. The JIT core (_calc_sparse_quantiles_core()) pads with zeros at the front of the sorted array.