laurel.utils package
Submodules
laurel.utils.align module
Profile alignment metrics for comparing temporal charging load distributions.
This module provides two complementary distance/similarity measures for comparing normalised load profiles (or any non-negative 1-D distributions):
calc_intersect_alignment()— intersection over union of two normalised histograms; ranges from 0 (no overlap) to 1 (identical profiles).calc_wasser_circle_distance()— Earth-Mover’s (Wasserstein-1) distance on a circular support, solved as a linear programme viacvxpy. Suitable for comparing time-of-day distributions where 23:00 and 01:00 are adjacent.
These metrics are used in reporting notebooks to quantify how well estimated load profiles match observed (validation) profiles.
- laurel.utils.align.calc_intersect_alignment(a, b)[source]
Compute the histogram-intersection similarity between two non-negative arrays.
Normalises both arrays to sum to 1, then returns the sum of element-wise minima — equivalent to the fraction of the distribution mass that the two profiles share. Returns
nanif either array sums to zero.- Parameters:
a (
ndarray) – Non-negative 1-D array (e.g. hourly load profile).b (
ndarray) – Non-negative 1-D array of the same length asa.
- Return type:
float- Returns:
Similarity score in [0, 1], or
nanif either input is all-zero.
- laurel.utils.align.calc_wasser_circle_distance(a, b)[source]
Compute the Wasserstein-1 distance between two distributions on a circular domain.
Uses a linear-programming formulation (via
cvxpy) with a ground metric that is the shorter arc-length on the unit circle, so the distance between bins wraps at the boundary (e.g. hour 23 and hour 1 are 2 bins apart). Both arrays are normalised to unit mass before solving. Returnsnanif either array sums to zero.- Parameters:
a (
ndarray) – Non-negative 1-D array representing the first distribution over equally-spaced bins on a circle.b (
ndarray) – Non-negative 1-D array of the same shape asa.
- Return type:
float- Returns:
Wasserstein-1 distance (in units of bins), or
nanif either input is all-zero.- Raises:
RuntimeError – If
aandbhave different shapes.
laurel.utils.data module
General-purpose DataFrame utilities shared across LAUREL pipelines.
This module provides helpers for merging, filtering, type-casting, and column-selecting pandas and Dask DataFrames, as well as a utility class for round-tripping a DataFrame through an integer index (needed before Numba JIT kernels) and a record-array conversion adapted from the pandas internals.
Key design decisions
Index preservation:
merge_dataframes_node()resets and restores the original index so that callers never have to worry about index loss after a left-join enrichment step.Dask compatibility: every function that accepts a pandas
DataFramealso accepts a DaskDataFramewhere performance constraints require it; the branching is handled internally so call-sites remain uniform.Integer merge key:
merge_on_int_cols()uses a Cantor-style encoding (id0 * max0 + id1) rather than a multi-column merge to avoid the expensiveset_indexthat Dask requires for keyed joins on non-index columns.
- class laurel.utils.data.IndexIntegerizer(int_col)[source]
Bases:
objectRound-trip a (multi)index through a dense integer encoding for Numba compatibility.
Numba JIT functions cannot operate on arbitrary pandas index values (strings, categoricals, MultiIndexes). This class converts the index to a compact integer sequence via
pandas.Index.factorize, allowing the array to be passed to a JIT kernel, then restores the original index values afterwards.Typical usage:
integerizer = IndexIntegerizer(int_col="veh_id_int") df_int = integerizer.integerize(df) result = numba_kernel(df_int) # operates on integer index df_restored = integerizer.deintegerize(result)
- deintegerize(df)[source]
Convert the integerized column back to the original form.
- Return type:
DataFrame
- property idx_names: str
- property int_col: str
- integerize(df)[source]
Convert the original (multi)index to an integer form of itself.
- Return type:
DataFrame
- property uniques: DataFrame
- laurel.utils.data.categorize_columns(df)[source]
Convert all
object-dtype columns topandas.Categoricalto reduce memory use.String columns stored as Python objects consume substantially more RAM than categorical columns with a small cardinality vocabulary (e.g. vehicle class, state code, hex cluster label). This function is applied after loading or joining any DataFrame that may carry such columns.
DwellSetinputs are handled transparently: only the underlyingDwellSet.dataDataFrame is modified; the wrapper is reconstructed viacopy_without_data()so metadata is preserved.- Parameters:
df (
DataFrame|DwellSet) – DataFrame or DwellSet whose object-typed columns should be converted.- Return type:
DataFrame|DwellSet- Returns:
Same type as
dfwith object columns replaced bypd.Categorical.
- laurel.utils.data.filter_by_vals_in_cols(df, params)[source]
Filter a DataFrame to rows where specified columns contain given values.
Applies a sequence of
isinfilters, combining them with AND or OR logic as specified per column. Optionally discards all columns not named in the filter spec. GeoDataFrames are handled transparently: the geometry column is always retained regardless ofkeep_only_filter_cols.- Parameters:
df (pd.DataFrame | gpd.GeoDataFrame) – DataFrame or GeoDataFrame to filter.
params (dict) –
Configuration dict with the following keys:
filters (
dict[str, dict]): Mapping of column name to a per-column filter spec. Each spec may contain:value_isin(list): Allowed values for this column.invert(bool, optional): IfTrue, keep rows where the column is not invalue_isin.joining_bool(str, optional):"AND"(default) or"OR"; controls how this column’s mask is combined with prior masks.
keep_only_filter_cols (
bool): IfTrue, return only the columns named infilters(plus geometry for GeoDataFrames).
- Return type:
pd.DataFrame | gpd.GeoDataFrame
- Returns:
Filtered DataFrame or GeoDataFrame.
- laurel.utils.data.get_basic_dtype_ser(ser)[source]
Cast a Series to its equivalent non-nullable NumPy dtype (
int64orfloat64).Pandas extension integer/float types (e.g.
Int64,Float32) cannot be passed directly to Numba JIT functions. This helper converts them to the corresponding plain NumPy dtype.- Parameters:
ser (
Series) – Series with an integer or float dtype.- Return type:
Series- Returns:
Series cast to
np.int64ornp.float64.- Raises:
RuntimeError – If
serhas neither an integer nor a float dtype.
- laurel.utils.data.get_merge_params(merge_params, right_df, *args)[source]
Restrict
merge_params["keep_right_columns"]to columns present inright_df.Before a merge, the config may list column names that do not exist in the current scenario’s right DataFrame (e.g. because a column appears only in some scenarios). This function intersects the desired columns with those actually available — including the index levels — so that
merge_dataframes_node()never tries to select a non-existent column. Additional column lists passed as positional*args(e.g. the activegroup_columnsparam sets) are also unioned into the target set.- Parameters:
merge_params (
dict) – Mutable config dict with at least akeep_right_columnskey listing the desired right-hand columns.right_df (
DataFrame) – The DataFrame that will be used as the right side of the merge. Its columns and index names are treated as available sources.*args (
list[list[str]]) – Extra lists of column names (e.g.params:substation.group_columns,params:county.group_columns) whose entries should also be kept if present inright_df.
- Return type:
dict- Returns:
The mutated
merge_paramsdict withkeep_right_columnsnarrowed to the intersection of requested and available columns.
- laurel.utils.data.get_multi_col_merger(df, src_cols, col_0_max)[source]
Build a single column to merge on which is a combination of two columns.
We achieve this using an encoding function (e.g. id1 * set1_size + id2). An alternative to this would be a hash.
The columns may not include an index.
- Return type:
pd.Series | dd.Series
- laurel.utils.data.merge_dataframes_node(left, right, params)[source]
Left-join metadata from
rightontoleft, preservingleft’s index.Designed for the common Kedro pattern where a large fact table (
left, possibly a Dask DataFrame) is enriched with a smaller lookup table (right, always in-memory pandas). Only the columns listed inparams["keep_right_columns"]are carried across; this avoids pulling unnecessary columns into the Dask graph.For Dask inputs, the join is executed partition-by-partition via
map_partitionsto avoid the expensive globalset_indexthat a standard Dask merge would require.rightis converted to a one-partition Dask collection and passed positionally so it becomes a single shared graph dependency rather than being re-embedded as an unmanaged, unspillable literal in every partition’s task.- Parameters:
left (pd.DataFrame | dd.DataFrame) – The large fact DataFrame (pandas or Dask) whose index is preserved.
right (pd.DataFrame) – The small metadata DataFrame to join onto
left.params (dict) –
Configuration dict with the following keys:
keep_right_columns (
list[str]): Columns fromright(plus its index) to include in the output. Must contain at least one entry.merge_kwargs (
dict): Keyword arguments forwarded verbatim topandas.DataFrame.merge(e.g.on,how).
- Return type:
pd.DataFrame | dd.DataFrame
- Returns:
Merged DataFrame with the same type and index as
leftand the selected columns fromrightappended.- Raises:
RuntimeError – If
leftis not a pandas or Dask DataFrame.RuntimeError – If
rightis not a pandas DataFrame.RuntimeError – If
keep_right_columnsis empty.
- laurel.utils.data.merge_on_int_cols(left, right, on, **kwargs)[source]
Merge a pandas DataFrame onto a Dask DataFrame keyed on integer column(s).
Dask’s native merge on non-index columns requires a global
set_index, which is expensive. This function avoids that cost by encoding up to two integer join columns into a single synthetic key (id0 * max(id0) + id1), performing a cheap index-based merge, then dropping the synthetic key.If the join column is already the Dask index, it is temporarily materialised as a column, merged, then restored as the index.
- Parameters:
left (
DataFrame) – Dask DataFrame to enrich (large, partitioned).right (
DataFrame) – pandas DataFrame to join ontoleft(small, in-memory).on (
str|list[str]) – Column name or list of up to two column names to join on.**kwargs – Additional keyword arguments forwarded to
dd.merge.
- Return type:
DataFrame- Returns:
Dask DataFrame with the same partitioning and index as
leftplus the columns fromright.- Raises:
NotImplementedError – If more than two join columns are supplied.
- laurel.utils.data.select_columns(df, params)[source]
Return
dfrestricted to the columns listed inparams["keep_cols"].For GeoDataFrames the geometry column is always appended to the selection so that spatial operations remain valid downstream.
- Parameters:
df (pd.DataFrame | gpd.GeoDataFrame) – DataFrame or GeoDataFrame to column-select.
params (dict) –
Configuration dict with the following key:
keep_cols (
str | list[str]): Column name(s) to retain.
- Return type:
pd.DataFrame | gpd.GeoDataFrame
- Returns:
DataFrame or GeoDataFrame containing only the requested columns (plus geometry for GeoDataFrames).
- laurel.utils.data.to_arrays(df, index=True, column_dtypes=None, index_dtypes=None)[source]
Convert a DataFrame to arrays suitable for constructing a NumPy recarray.
Extracts index and column data from
dfas a list of NumPy arrays, along with corresponding field names and dtype objects. The returned triple can be passed directly tonp.rec.fromarrays.- Parameters:
df (
DataFrame) – DataFrame to convert.index (
bool) – IfTrue, include the index as the first field(s); the field name is taken from the index label, or'index'if unlabelled. Defaults toTrue.column_dtypes – If a string or type, the data type to store all columns. If a dictionary, a mapping of column names and zero-indexed positions to specific data types. Defaults to
None(infer from array dtype).index_dtypes – If a string or type, the data type to store all index levels. If a dictionary, a mapping of index level names and zero-indexed positions to specific data types. Applied only if
index=True. Defaults toNone.
- Return type:
tuple- Returns:
Three-tuple
(arrays, names, formats)wherearraysis a list of NumPy arrays (one per included index level then column),namesis the corresponding list of field name strings, andformatsis the list of dtype objects.- Raises:
ValueError – If a
dtype_mappingentry is not a type, anumpy.dtype, or a string.
laurel.utils.distributed module
Helpers for materialising deferred Dask data as a Kedro node.
Kedro pipelines that process large DwellSets or partitioned datasets use Dask
for parallelism. Cluster lifecycle is now managed by
DaskClusterHook, which starts and stops a
LocalCluster around every pipeline run. This module exposes only the
load_in_memory_node() helper, which forces a Dask DataFrame (or a
DwellSet backed by one) into RAM as a regular Kedro node.
Key design decisions
load_in_memory_node: Used to materialise deferred Dask computations before operations that require random access or pandas-only APIs (e.g. Numba JIT calls, index-based joins). Returns the input unchanged if it is already backed by pandas.
- laurel.utils.distributed.load_in_memory_node(ddf)[source]
Force a Dask DataFrame (or DwellSet) into in-memory pandas form.
Used as a Kedro node to materialise a deferred Dask computation before operations that require random access or pandas-only APIs (e.g. Numba JIT calls, index-based joins). If the input is already a pandas DataFrame or a DwellSet backed by one, it is returned unchanged.
- Parameters:
ddf (dd.DataFrame | DwellSet) – A Dask
DataFrameor aDwellSetwhosedataattribute may be a DaskDataFrame.- Return type:
pd.DataFrame | DwellSet
- Returns:
A pandas
DataFrame, or aDwellSetwhosedataattribute is a pandasDataFrame.- Raises:
NotImplementedError – If
ddfis neither a Dask/pandas DataFrame nor aDwellSet.
laurel.utils.events module
Event detection from periodic observations using Numba JIT acceleration.
An event is a semi-contiguous run of observations that satisfy a boolean
inclusion criterion, where short gaps (up to max_time_elapsed) between
qualifying observations are bridged into a single event. This abstraction is
used in LAUREL to identify contiguous charging intervals (periods when a
vehicle is actively charging) from a time-ordered sequence of dwell
observations.
The implementation follows a three-layer pattern:
get_events()— pandas entry point; adds duration seconds, groups by entity, calls the wrapper.get_events_wrapper()— bridges pandas group DataFrames to NumPy arrays for Numba.get_events_core()— Numba@njitkernel; assigns an integer event ID to every observation in a single-entity sequence.
Key design decisions
Zero as no-event sentinel: Event IDs start at 1; observations not belonging to any event receive ID 0. Using 0 (rather than NaN) keeps the column as an integer dtype, which simplifies downstream merges.
Interval-beginning timestamps: The algorithm assumes each observation’s timestamp marks the start of its duration interval, so elapsed time is accumulated using the current row’s
dur_colrather than the gap to the next row.
- laurel.utils.events.get_events(df, include_col, dur_col, grp_col, out_col='event_id', max_time_elapsed=Timedelta('0 days 00:00:00'))[source]
Get an event id column which gives the indices for events based on periodic observations.
Events are defined as semi-contiguous stretches of a value matching the criterion given by obs_in_event. The breaks in an event can only be as long as max_time_elapsed.
This function depends on sorting by time within each group, and that groups are each one block of rows.
- Parameters:
df (
DataFrame) – DataFrame to add the event_id column toinclude_col (
str) – the name of a boolean series which is True for every row (observation) that should be in an event and False for every row (observation) that should NOT be in an event.dur_col (
str) – the name of a series of durations (as pandas TimeDeltas) of the events given in include_colgrp_col (
str) – the name of a series of markers for groups of observations, usually this would be a series of integer ids, but if None, then all observations are assumed to come from the same group.max_time_elapsed (
Timedelta) – pandas TimeDelta giving the maximum time between events for them to be combined into a single event.
- Return type:
DataFrame
Returns: a dataframe with a new column which gives an index of which event a particular observation is a part of (or zero if the observation is part of no event). Zero is used instead of a null value to mark the non-events because it allows the new column to be cast to an integer type, which facilitates merging.
- laurel.utils.events.get_events_core(include, secs_elapsed, max_secs_elapsed=0.0)
Get a ndarray which gives the indices for events based on periodic observations.
Events are defined as semi-contiguous stretches of a value matching the criterion given by obs_in_event. The breaks in an event can only be as long as max_time_elapsed.
Assues interval-beginning time stamps, so events are marked at their beginning, with the duration assumed to follow.
- Return type:
ndarray
- laurel.utils.events.get_events_wrapper(grp, include_col, secs_elapsed_col, out_col, max_time_elapsed)[source]
Convert a group DataFrame into NumPy arrays and delegate to the JIT kernel.
Extracts boolean and float arrays from the group and calls
get_events_core(), writing the resulting event-ID array back as a column.- Parameters:
grp (
DataFrame) – Single-entity sub-DataFrame, time-ordered within the entity.include_col (
str) – Boolean column indicating event membership.secs_elapsed_col (
str) – Float column of observation durations in seconds.out_col (
str) – Column name to write the integer event IDs into.max_time_elapsed (
Timedelta) – Maximum gap (aspd.Timedelta) to bridge within an event.
- Return type:
DataFrame- Returns:
grpwithout_colupdated to integer event IDs.
laurel.utils.geo module
Geometric utilities for vehicle operating-radius and time-weighted center calculations.
This module provides the spatial computation functions needed to characterise each vehicle’s spatial footprint: where it spends most of its dwell time (time-weighted center) and how far it ranges from that center (operating radius as half the diameter of the convex hull of its dwell locations).
Key functions:
find_time_weighted_centers()— weighted centroid of projected dwell coordinates.calc_operating_radius()— convex-hull diameter via rotating calipers, using Haversine distances for geographic accuracy.calc_haversine_dist()andcalc_max_dist_calipers()— Numba JIT inner kernels.
Key design decisions
Projected coordinates required:
find_time_weighted_centers()requires the GeoDataFrame to be in a projected (metric) CRS so that the weighted mean of easting/northing coordinates is geometrically meaningful. The caller is responsible for reprojection.Rotating calipers: The maximum pairwise distance across a convex hull is computed in O(n) using rotating calipers rather than O(n²) brute-force. For degenerate cases (single point or line), the diameter is 0 or the direct Haversine distance respectively.
Numba JIT: Both
calc_haversine_dist()andcalc_max_dist_calipers()are JIT-compiled for performance when called across thousands of vehicles.
- laurel.utils.geo.calc_haversine_dist(pt1, pt2, radius)
Calculate the Haversine distance between two points on Earth’s surface.
Assumes (longitude, latitude) points. Each row is a point.
- Return type:
ndarray[tuple[int,...],dtype[floating]]
- laurel.utils.geo.calc_max_dist_calipers(hull_lonlat, radius)
Calculate the maximum distance across a convex hull using rotating calipers.
- Return type:
float
- laurel.utils.geo.calc_operating_radius(points)[source]
Estimate a vehicle’s operating radius as half the diameter of its convex hull.
The diameter is the maximum pairwise distance across the convex hull of all dwell locations, computed using the rotating-calipers algorithm (
calc_max_dist_calipers()) for efficiency. Haversine distances are used so the result is in miles regardless of the CRS ofpoints.Degenerate cases are handled explicitly:
A single point → radius 0.
Two points (a line) → half the direct Haversine distance.
A polygon → rotating-calipers diameter / 2.
- Parameters:
points (
GeoSeries) – GeoSeries of ShapelyPointgeometries (longitude, latitude) representing the vehicle’s dwell locations.- Return type:
float- Returns:
Operating radius in miles.
- laurel.utils.geo.find_time_weighted_centers(gdf, grp_col, weight_col, center_col='centers')[source]
Compute the dwell-time-weighted geographic center for each vehicle.
For each group identified by
grp_col, computes the weighted mean of the projected easting and northing coordinates, where the weight isweight_col(typically dwell duration). Returns a GeoDataFrame of center points, one row per group.WARNING: This function may be very slow in Dask if the GeoDataFrame is not indexed on
grp_col.- Parameters:
gdf (
GeoDataFrame) – Projected GeoDataFrame (metric CRS required) of vehicle dwells, with one row per dwell and a geometry column of point locations.grp_col (
str) – Column to group by (typically a vehicle ID).weight_col (
str) – Column of dwell durations or other non-negative weights.center_col (
str) – Name of the geometry column in the output GeoDataFrame. Defaults to"centers".
- Return type:
GeoDataFrame- Returns:
GeoDataFrame indexed by
grp_colwith a single geometry column (center_col) of weighted centroid points in the same projected CRS.- Raises:
RuntimeError – If
gdfhas no CRS or its CRS is not projected.
laurel.utils.h3 module
H3 hexagonal grid utilities: coordinate conversion, geometry construction, and polygon-cell mapping.
This module provides the spatial primitives needed to work with Uber’s H3
hierarchical geospatial index throughout LAUREL. All H3 operations use
resolution 8 (cell diameter ≈ 0.46 km) and the WGS-84 geographic CRS
(EPSG:4326).
Key functions:
Coordinate conversion:
cells_to_points(),cells_to_polygons(),coords_to_cells().GeoDataFrame construction:
add_geometries()(handles both pandas and Dask inputs).Region operations:
cells_to_poly()(union cells into a region shape),cells_to_region_polygons(),region_polygons_to_cells().
Key design decisions
Dask compatibility:
add_geometries()converts a Dask DataFrame todask_geopandasviamap_partitionsso that geometry attachment can be parallelised; the CRS is set on the whole frame after partition mapping.H3 integer API: The module primarily uses
h3.api.numpy_int(uint64 cell IDs) for Numba/NumPy compatibility; string-based H3 APIs are imported only where needed (e.g.coords_to_cells()returns uint64).
- laurel.utils.h3.add_geometries(data, hex_col, geom_type='point')[source]
Attach H3 geometries to a DataFrame, returning a GeoDataFrame.
Converts each H3 cell in
hex_colto either a centroid point or the full hexagon polygon, then attaches the resultingGeoSeriesas the geometry column. Handles both pandas and Dask DataFrames; for Dask inputs the geometry is attached per-partition viamap_partitions.- Parameters:
data (pd.DataFrame | dd.DataFrame) – pandas or Dask DataFrame with an H3 integer cell ID column.
hex_col (str) – Name of the column (or index level) holding H3 uint64 cell IDs.
geom_type (str) –
"point"for centroid points or"polygon"for full hexagon polygons.
- Return type:
gpd.GeoDataFrame | dgpd.GeoDataFrame
- Returns:
GeoDataFrame (or Dask GeoDataFrame) with CRS
EPSG:4326and ageometrycolumn of Shapely objects.- Raises:
RuntimeError – If
geom_typeis neither"point"nor"polygon".RuntimeError – If
datais neither a pandas nor a Dask DataFrame.
- laurel.utils.h3.cells_to_points(s)[source]
Creates a like-indexed GeoSeries of points from a series of h3 cells.
- Return type:
GeoSeries
- laurel.utils.h3.cells_to_poly(hser)[source]
Generate a region (multi-)polygon from a series of hexagon ids.
- Return type:
H3Shape
- This will often be useful in a groupby setting. For example:
df.groupby(“region_id”)[“hex_id”].agg(cells_to_poly)
- laurel.utils.h3.cells_to_polygons(s)[source]
Creates a like-indexed GeoSeries of polygons from a series of h3 cells.
- Return type:
GeoSeries
- laurel.utils.h3.cells_to_region_polygons(corresp, hex_col, region_col)[source]
Build one (multi-)polygon per region from a hex→region correspondence table.
Groups
correspbyregion_col, unions the H3 cells in each group into a contiguous region shape viacells_to_poly(), and returns a GeoDataFrame indexed by region.- Parameters:
corresp (
DataFrame) – DataFrame with at leasthex_colandregion_colcolumns.hex_col (
str) – Name of the H3 uint64 cell ID column.region_col (
str) – Name of the region identifier column to group by.
- Return type:
GeoDataFrame- Returns:
GeoDataFrame with columns
[region_col, "geometry"]and CRSEPSG:4326.
- laurel.utils.h3.coords_to_cells(lat, lng, res)[source]
Convert parallel latitude/longitude arrays to H3 uint64 cell IDs at
res.- Parameters:
lat (
ndarray) – 1-D array of latitudes in decimal degrees.lng (
ndarray) – 1-D array of longitudes in decimal degrees (same length aslat).res (
int) – H3 resolution (0–15).
- Return type:
ndarray- Returns:
1-D
uint64array of H3 cell IDs, same length aslat.
- laurel.utils.h3.region_polygons_to_cells(geos, grp_cols, hex_col)[source]
Explode a GeoDataFrame of (multi-)polygons to one row per H3 cell they cover.
For each polygon in
geos, enumerates all H3 resolution-8 cells whose centroids fall within the polygon viah3.geo_to_cells, then explodes the result to long form. Empty or null geometries are silently dropped.- Parameters:
geos (
GeoDataFrame) – GeoDataFrame of polygon or multipolygon geometries.grp_cols (
str|list[str]) – Column name(s) identifying each region (carried through to the output).hex_col (
str) – Name of the output column for H3 uint64 cell IDs.
- Return type:
DataFrame- Returns:
Long DataFrame with columns
grp_cols + [hex_col]and a default integer index.
- laurel.utils.h3.str_to_h3(s)[source]
Convert a Series of H3 hex-string cell IDs to uint64 integer IDs.
- Return type:
Series
- laurel.utils.h3.to_geospatial(df, params)[source]
Kedro node wrapper: attach H3 geometries to a DataFrame.
- Parameters:
df (
DataFrame) – pandas DataFrame with an H3 cell ID column.params (
dict) – Keyword arguments forwarded toadd_geometries()(hex_col, optionallygeom_type).
- Return type:
GeoDataFrame- Returns:
GeoDataFrame with geometry column in
EPSG:4326.
laurel.utils.hex_neighbors module
Neighbor-embedding utility for H3 hexagonal grids.
Computes spatially smoothed feature vectors by averaging the embeddings of
neighboring H3 cells. Used in describe_locations to augment per-hex
employment matrices with information from adjacent hexes before K-Means
clustering, reducing the effect of sparse or missing establishment data in
individual cells.
The averaging is implemented via sparse matrix multiplication:
neighbor_matrix @ embeddings / n_neighbors, where the neighbor matrix is
built once and reused. Hexes whose neighbors fall outside the observed set
are averaged over only the neighbors that exist.
Key design decisions
Sparse CSR matrix: The neighborhood structure is stored as a
scipy.sparse.csr_array(one row per hex, non-zero entries at neighbor positions) so the matrix-multiply is O(n × avg_neighbors) rather than O(n²).Fixed denominator: The denominator is taken from the neighbor count of the first hex (assumed uniform across the grid), which avoids a per-row division but means edge hexes with fewer in-set neighbors receive a slightly downweighted average.
- laurel.utils.hex_neighbors.get_neighbor_embeddings(hexes, embs, include_center=False, distance=1)[source]
Average the feature embeddings of the neighbors of each H3 hexagon.
For each hex in
hexes, looks up its neighbors withindistancerings, finds those neighbors that exist inhexes, and averages their rows inembs. Hexes with no in-set neighbors receive an all-zero embedding.The neighborhood adjacency structure is built as a sparse CSR matrix and the averaging is performed as a single sparse matrix multiply.
- Parameters:
hexes (
ndarray[uint64]) – 1-D uint64 array of H3 cell IDs (one per observation).embs (
ndarray) – 2-D float array of shape(n_obs, n_features)— the embedding for each hex in the same order ashexes.include_center (
bool) – IfTrue, include the hex itself when averaging its neighbors. Defaults toFalse(ring neighbors only).distance (
int) – Number of H3 grid rings to include as neighbors.distance=1means the 6 immediately adjacent hexes.
- Return type:
ndarray- Returns:
2-D float array of shape
(n_obs, n_features)— the averaged neighbor embeddings, one row per hex inhexes.
- laurel.utils.hex_neighbors.get_ngbr_idxs(hex, hex_to_idx, include_center, distance)[source]
Return the row indices (within
hexes) of the neighbors ofhex.Only neighbors that appear in
hex_to_idx(i.e. have an observed embedding) are returned; out-of-set neighbors are silently skipped.- Parameters:
hex (
uint64) – The H3 uint64 cell ID to look up neighbors for.hex_to_idx (
dict[uint64,uint64]) – Mapping from H3 cell ID to its position in thehexesarray.include_center (
bool) – Whether to includehexitself.distance (
int) – Ring distance passed toget_ngbrs().
- Return type:
list[uint64]- Returns:
List of integer indices into the
hexes/embsarrays.
- laurel.utils.hex_neighbors.get_ngbrs(hex, include_center, distance)[source]
Return the H3 neighbor cell IDs for a single hex at a given ring distance.
- Parameters:
hex (
uint64) – H3 uint64 cell ID.include_center (
bool) – IfTrue, includehexin the returned set.distance (
int) – Number of grid rings.distance=1returns the 6 adjacent hexes (or 7 withinclude_center=True).
- Return type:
ndarray[uint64]- Returns:
1-D uint64 array of neighbor cell IDs.
laurel.utils.infra module
Utilities for building substation-to-hex correspondence tables.
Provides two spatial helpers used in the describe_locations pipeline:
build_utility_territory()— constructs a rough convex-hull territory polygon for a utility (currently PG&E) from its substation coordinates.build_nearest_infra_corresp()— assigns each H3 hexagon to its nearest substation via a spatial nearest-neighbor join, producing the hex→substation correspondence table used throughout the evaluate_impacts pipeline.
- laurel.utils.infra.build_nearest_infra_corresp(hexes, infra, params)[source]
Assign each H3 hexagon to its geographically nearest substation.
Converts hex centroids to points (via
cells_to_points()), reprojects to the infra CRS, performs asjoin_nearestagainst the substation GeoDataFrame, and renames columns according toparams["substation_col_renamer"]. Duplicate hex assignments (which can arise when two substations are equidistant) are dropped with a warning.- Parameters:
hexes (
DataFrame) – DataFrame with an H3 cell ID column (params["hex_col"]) and optionally a named index.infra (
GeoDataFrame) – GeoDataFrame of substation point geometries.params (
dict) –Configuration dict with the following keys:
hex_col (
str): Name of the H3 uint64 cell ID column inhexes.substation_col_renamer (
dict[str, str]): Mapping from output column names to the corresponding column names ininfra(i.e.{desired_name: infra_col_name}).
- Return type:
DataFrame- Returns:
DataFrame with one row per unique hex, containing the hex ID column plus the renamed substation attribute columns, indexed as in
hexes.
- laurel.utils.infra.build_utility_territory(infra, params)[source]
Build an approximate utility service-territory polygon from substation points.
Unions all substation geometries, takes the convex hull, and buffers it by
params["buffer_dist_meters"]to produce a single polygon representing the utility’s rough service territory. Currently hard-coded for PG&E.- Parameters:
infra (
GeoDataFrame) – GeoDataFrame of substation point geometries in a projected CRS.params (
dict) –Configuration dict with the following key:
buffer_dist_meters (
float): Buffer distance in metres to apply around the convex hull.
- Return type:
GeoDataFrame- Returns:
Single-row GeoDataFrame with columns
["utility", "territory"]and the same CRS asinfra.
laurel.utils.location_grouping module
Evaluator for dwell-count uniformity within location groups (TAZ clusters).
When hexagons are grouped into freight-activity clusters for the K-Means
classification step, an ideal clustering assigns roughly equal numbers of
observed dwells to every location within a group. This module provides
LocGroupingUniformityEvaluator, which computes an obs/uniform
ratio for each location (how many times more dwells were observed at this
location than expected under a uniform distribution within the group) and
exposes summary statistics at multiple levels of aggregation.
This evaluator is used in exploratory notebooks to validate that the K-Means clustering produces geographically coherent groups rather than concentrating all observed activity in a few locations per cluster.
- class laurel.utils.location_grouping.LocGroupingUniformityEvaluator(dwell_locs, loc_groups)[source]
Bases:
objectEvaluate dwell-count uniformity within location groups and serve multiple summaries.
On construction, computes for every location:
n_dwells_observed: actual dwell count at this location.n_dwells_uniform: expected count if dwells were spread evenly across all locations in the same group.obs_uniform_ratio: the ratio of the two.
After setting a cutoff via
set_cutoff(), thesummarymethod returns statistics at three granularities: raw boolean mask, scalar overall fraction, or per-group breakdown.- Args (constructor):
dwell_locs: Series of location IDs, one entry per dwell observation. loc_groups: Series indexed by location ID, values are group labels.
- summary(kind='overall')[source]
Return a uniformity summary at the requested aggregation level.
- Parameters:
kind (
Literal['raw','overall','group']) –One of:
"raw"— boolean Series, one entry per location, indicating whether itsobs_uniform_ratioexceeds the cutoff."overall"— scalar fraction of locations exceeding the cutoff across the whole dataset."group"— per-group DataFrame with columnsexceeds_cutoff,group_size,uniform_dwell_rate,group_frac, andexceeds_cutoff_frac.
- Return type:
float|Series|DataFrame- Returns:
Summary at the requested level.
- Raises:
ValueError – If
set_cutoff()has not been called yet.NotImplementedError – If
kindis not one of the three supported values.
laurel.utils.logging module
Logging utilities for suppressing verbose third-party output during pipeline runs.
Some libraries (e.g. osmium, dask) emit large volumes of INFO or
WARNING messages that obscure meaningful pipeline output. This module provides
a context manager to temporarily silence the root logger at a configurable
level.
- class laurel.utils.logging.SuppressLogs(level=50)[source]
Bases:
objectContext manager that globally disables logging up to a given severity level.
Saves and restores the root logger’s disabled level on entry and exit, so temporary suppression does not leak past the
withblock.- Parameters:
level – Logging level at and below which messages are suppressed. Defaults to
logging.CRITICAL(suppress everything).
Example:
with SuppressLogs(logging.WARNING): noisy_library_call() # INFO and WARNING messages suppressed
laurel.utils.mapping module
Interactive map visualisation for vehicle dwell sequences.
Provides map_dwells(), a thin wrapper around geopandas.GeoDataFrame.explore
that renders dwell locations as hexagon polygons and, optionally, draws
trajectory lines connecting consecutive dwell centroids. Intended for use in
exploratory notebooks to inspect individual vehicle routing patterns.
- laurel.utils.mapping.map_dwells(df, hue_col, hex_col, trajectories=True, **kwargs)[source]
Render dwell locations as an interactive map with optional trajectory lines.
Converts H3 cell IDs to hexagon polygons, colours them by
hue_col, and displays the result viafolium(throughgeopandas.explore). Iftrajectories=True, a second layer ofLineStringsegments is drawn connecting the representative point of each dwell to that of the next, giving a visual impression of the vehicle’s route.- Parameters:
df (
DataFrame) – Time-ordered DataFrame of dwell observations for one or more vehicles, with at leasthex_colandhue_colpresent.hue_col (
str) – Column used to colour hexagons (e.g."charging_mode"). Categorical columns are cast tostrbefore plotting.hex_col (
str) – Column of H3 cell IDs (integer or string format both accepted).trajectories (
bool) – IfTrue(default), draw line segments between consecutive dwell centroids on a second map layer.**kwargs – Additional keyword arguments forwarded to
GeoDataFrame.explore(e.g.cmap,tooltip,tiles).
- Returns:
A
folium.Mapobject that can be displayed inline in a notebook.
laurel.utils.mode_masks module
Bitmask encoding/decoding for charging-mode availability vectors.
The charging-choice algorithm (see laurel.models.charging_algorithms)
needs to know, for each dwell, which of the N charging modes are available
(depot, destination, truck stop, etc.). Storing one boolean column per mode
would be expensive at scale; instead, the availability of all modes is packed
into a single uint64 bitmask column, where bit j is set if mode j
is available.
This module provides three functions for working with these bitmasks:
bool_arr_to_bits()— vectorised encode: boolean array → uint64 bitmask(s).bits_to_bool_arr()— vectorised decode: uint64 bitmask(s) → 2-D boolean array.bits_to_bool_vec()— scalar Numba JIT decode: single uint64 → 1-D boolean vector.
The MAX_CHARGE_MODES = 64 limit follows from the uint64 representation.
- laurel.utils.mode_masks.bits_to_bool_arr(bits, n_modes)[source]
Vectorized decode of uint64 bitmask(s) into a 2D boolean availability array.
- Parameters:
bits (np.ndarray) – 0-D or 1-D array of uint64 bitmasks. If 0-D/1-D, treated as a single row or vector of rows respectively. A 2-D array is not semantically meaningful here (would imply matrix of masks) and will raise.
n_modes (int) – Number of mode positions (columns) to decode. Must be <= MAX_CHARGE_MODES.
- Returns:
2-D boolean array of shape (n_items, n_modes) where element [i, j] is True if bit j of bits[i] is set.
- Return type:
np.ndarray
- laurel.utils.mode_masks.bits_to_bool_vec(bitmask, n_modes)
Decode a single uint64 bitmask to a 1-D boolean vector (Numba nopython).
- Parameters:
bitmask (np.uint64 or int) – Single bitmask whose bits indicate availability of modes.
n_modes (int) – Number of mode positions (length of the output). Must be <= MAX_CHARGE_MODES.
- Returns:
1-D boolean array of length n_modes where element j is True if bit j is set.
- Return type:
np.ndarray
- laurel.utils.mode_masks.bool_arr_to_bits(arr)[source]
Vectorized encode of boolean array (0D/1D/2D) to per-row uint64 bitmasks.
Normalizes dimensionality to 2D, then uses NumPy broadcasting with powers-of-two weights and a row-wise sum to compute the bitmask. Returns a 1D uint64 mask array. Empty inputs handled gracefully.
- Return type:
ndarray
laurel.utils.naics module
NAICS code matching utilities (Numba JIT-compiled).
Provides get_naics_leaf_class(), which maps arbitrary-depth NAICS codes
to a predefined set of “leaf” codes by iteratively truncating the code from the
right until a match is found. Used in describe_locations to collapse
the full 6-to-8-digit Data Axle NAICS codes to the coarser leaf classes needed
for freight-activity clustering.
- laurel.utils.naics.get_naics_leaf_class(codes, leaves, src_digits=8, fill_leaf=None)
Map each NAICS code to the most specific matching leaf code.
Iteratively right-truncates each code (by integer division by 10) and checks it against the leaf set until every code matches a leaf or has been truncated to zero. This implements a hierarchical NAICS rollup: a code that matches no leaf at 8 digits is tried at 6, then 4, then 2 digits.
- Parameters:
codes (
ndarray[int]) – 1-D integer array of NAICS codes to classify.leaves (
ndarray[int]) – 1-D integer array of accepted leaf codes. Each leaf is matched at most once per pass, so the order ofleavesdoes not matter.src_digits (
int) – Not currently used; reserved for future digit-normalisation.fill_leaf (
int|None) – If notNone, codes that match no leaf are assigned this value instead of raising. Useful for catch-all categories.
- Return type:
ndarray[int]- Returns:
1-D integer array, same shape as
codes, containing the matched leaf code for each input.- Raises:
ValueError – If any code remains unmatched after full truncation and
fill_leafisNone.
laurel.utils.open_street_map module
OpenStreetMap PBF extraction utilities using the osmium library.
Provides helpers for filtering OSM PBF files by tag patterns and reading the
matching features into a GeoDataFrame. Used in describe_locations to
extract truck-stop nodes and warehouse polygons from the continental U.S. OSM
extract.
The extraction follows a two-pass strategy:
Filter pass: scan the PBF with tag-based filters, collect back-references (way nodes, relation members), and write matching objects to a temporary PBF via
osmium.BackReferenceWriter.Read pass: re-scan the temporary PBF with location resolution (
with_locations()) and theGeoInterfaceFilterto materialise geometries, then load into a GeoDataFrame.
This two-pass approach is required because OSM PBFs store node coordinates separately from way/relation references; the back-reference writer resolves those links without loading the full planet file into memory.
- class laurel.utils.open_street_map.RegexTagFilter(tag, pattern)[source]
Bases:
objectosmiumhandler that retains OSM objects whose tag value matches a regex.Implements the
osmium.BaseHandlerinterface (node,way,relationmethods) so it can be chained viaosmium.FileProcessor.with_filter(). An object passes the filter if its tag is present and the tag value matches the compiled pattern. Objects with the tag absent are excluded.- Parameters:
tag (
str) – OSM tag key to inspect (e.g."amenity","name").pattern (
str) – Regular expression pattern; matching is case-insensitive. A pattern match causes the object to be kept.
- laurel.utils.open_street_map.get_gdf_from_filtered_osm(osm_path, filters, tags, temp_path)[source]
Extract tag-filtered OSM features from a PBF file into a GeoDataFrame.
Uses the two-pass strategy described in the module docstring: first writes matching objects (with back-references resolved) to
temp_path, then re-reads with location resolution to materialise geometries.- Parameters:
osm_path (
Path) – Path to the source OSM PBF file (e.g. the continental U.S. extract).filters (
list[BaseHandler]) – List of filter handlers (e.g.RegexTagFilterinstances) applied to both passes.tags (
list[str]) – List of OSM tag keys to include as columns in the output GeoDataFrame (forwarded toGeoInterfaceFilter).temp_path (
Path) – Writable path for the intermediate filtered PBF. Overwritten if it already exists; its parent directory is created if needed.
- Return type:
GeoDataFrame- Returns:
GeoDataFrame of matching features in
EPSG:4326.
- laurel.utils.open_street_map.processor_factory(pbf_path, filters, with_locations=False, **geo_int_kwargs)[source]
Build a configured
osmium.FileProcessorwith filters and optional geometry.- Parameters:
pbf_path (
Path) – Path to the input OSM PBF file.filters (
list[BaseHandler]) – List ofosmium.BaseHandler-compatible filter objects applied in order viawith_filter().with_locations – If
True, attaches a location index (needed to resolve way/relation node coordinates) and appends aGeoInterfaceFilter.**geo_int_kwargs – Additional keyword arguments forwarded to
osmium.filter.GeoInterfaceFilter(e.g.tags).
- Return type:
FileProcessor- Returns:
Configured
osmium.FileProcessorready for iteration.
laurel.utils.params module
Utilities for setting per-entity parameters and extracting scenario configs.
In LAUREL, each vehicle (and, in some sub-pipelines, each hexagon) carries its own parameter values — battery size, charging power, random seed — stored as columns on the entity DataFrame. This module provides the helpers needed to populate those columns from YAML parameter dicts, to extract select parameters from nested scenario configs for reporting, and to import classes by dotted-path string (used for dynamically loading model components from YAML).
Key design decisions
Three param patterns:
set_entity_params()recognises three shapes of parameter value: (1) a flat scalar/string applied uniformly to all entities, (2) a{id_columns, values}dict that maps entity-level ids to different values via a left-join, and (3) a specialrandom_seedpattern that derives a per-entity seed from a master seed plus an entity ID, ensuring reproducible but independent stochasticity per vehicle.Integrity check on merge-type params: if any entity fails to match the provided lookup table,
set_entity_params()raises immediately rather than silently propagating NaNs.
- laurel.utils.params.build_df_from_dict(d, id_cols, value_col)[source]
Build a flat DataFrame from a uniformly-nested dict, with one column per id level.
Recursively expands nested dicts of uniform depth into a DataFrame whose first columns are the nested keys (matching
id_cols) and whose last column is the leaf value (value_col). Leaf values may be scalars or lists (the latter are stored as a single array-valued column).- Parameters:
d (
dict) – Nested dict of uniform depth. Keys at each level become one id column.id_cols (
list[str]) – Column names for the key levels, in nesting order. Must have length equal to the nesting depth ofd.value_col (
str) – Name for the leaf-value column.
- Return type:
DataFrame- Returns:
Flat DataFrame with columns
id_cols + [value_col].
- laurel.utils.params.extract_params(params, key_map)[source]
Extract the parameters of interest from a config for this scenario.
- Parameters:
params (
dict) – the dict of parameters used directly by kedrokey_map (
dict) – the dict of (reporting key, kedro parameter key) pairs. The kedro parameter keys may be tuples of any length, which will be interpreted as the key at each level of the dictionary.
- Return type:
dict
Returns: A dictionary of only the selected parameters which is only one level deep.
- laurel.utils.params.flatten_dict(d, parent_key=None, sep='_')[source]
Flatten a nested dict to a single level using
sep-joined keys.- Parameters:
d (
dict) – Arbitrarily nested dictionary.parent_key (
str) – Prefix to prepend to all keys at this level (used in recursion; passNonefor the top-level call).sep (
str) – Separator inserted between parent and child key names.
- Return type:
dict- Returns:
Single-level dict whose keys are
sep-joined paths through the original nesting.
- laurel.utils.params.import_from_config(import_path)[source]
Import a Python class (or any object) from a dotted import path string.
Allows YAML configuration files to specify model components by their fully qualified Python path (e.g.
"laurel.models.charging.LinearCharger"), which is then imported at runtime.- Parameters:
import_path (
str) – Fully qualified dotted path to the object (e.g."package.module.ClassName").- Returns:
The imported class or object.
- laurel.utils.params.set_entity_params(entities, params)[source]
Attach scenario parameters to an entity DataFrame as new columns.
Iterates over each key–value pair in
paramsand adds the corresponding column toentitiesaccording to one of three patterns:Merge-type — if the value is a dict with keys
id_columnsandvalues, builds a lookup table viabuild_df_from_dict()and left-joins it ontoentities. Raises if any entity is unmatched.Random seed — if the key is
"random_seed", computesentity[seed_id_col] + master_seedso every entity has an independent but deterministic seed.Scalar — any other value is flattened (nested dicts become
"parent_child"column names) and broadcast uniformly.
DwellSetinputs are handled transparently: the underlying DataFrame is modified and wrapped back into the originalDwellSet.- Parameters:
entities (
DataFrame|DwellSet) – DataFrame or DwellSet of entities to parameterise.params (
dict) –Mapping of column name to value. Each value may be:
A scalar (int, float, str) applied uniformly to all rows.
A nested dict, which is flattened with
_separators.A dict
{"id_columns": [...], "values": {...}}for entity- specific lookups (seebuild_df_from_dict()).A dict
{"seed_id_col": str, "master_seed": int}when the key is"random_seed".
- Return type:
DataFrame|DwellSet- Returns:
entitieswith one new column per key inparams.- Raises:
RuntimeError – If a merge-type param does not cover all entities.
- laurel.utils.params.tabularize_params(cfgs, read_keys, idx_name)[source]
Read configurations from their partitions into a DataFrame.
- Parameters:
cfgs (
dict[Any,dict]) – The dictionary of parameters for each scenario in a set, with the key representing the scenario identifier.read_keys (
dict[str,list[str]]) – The sequences of parameters to read from each of the the multi-level dictionaries within cfgs.idx_name (
str) – The name to give the resulting DataFrame index.
- Return type:
DataFrame
Returns: DataFrame of select parameters from each scenario, indexed by scenario identifier.
laurel.utils.plotting module
Plotting utilities for temporal load profiles and quantile bands.
Provides two components for visualising charging load profiles in reporting notebooks:
ProfileDensifier— fills missing time-steps in a sparse profile DataFrame so thatmatplotlib/seabornplots render without gaps.plot_quantile_bands()— overlays shaded confidence bands on aseaborn.FacetGridfor a set of quantile columns symmetric around the median.
- class laurel.utils.plotting.ProfileDensifier(time_col, freq, dur='1d')[source]
Bases:
objectClass for densifying sparse temporal profiles using time groupers.
Provides methods to create reusable time groupers and densify sparse DataFrames by filling missing time points with specified values.
- __init__(time_col, freq, dur='1d')[source]
Initialize ProfileDensifier with time configuration.
- Parameters:
time_col (
str) – Name of the column containing time/datetime values.freq (
str) – Frequency string for time series generation (e.g., “15min”, “1h”).dur (
str) – Duration string defining the total time span (default: “1d”).
- create_grouper()[source]
Create and return configured AdaptiveTimeGrouper for external use.
- Return type:
AdaptiveTimeGrouper- Returns:
Configured AdaptiveTimeGrouper instance that can be used independently.
- densify(sparse, grp_cols, value_cols, fill_value=0.0)[source]
Densify sparse profiles by filling missing time points.
- Parameters:
sparse (
DataFrame) – Sparse DataFrame containing observed data with missing time points.grp_cols (
list[object]) – List of column names to group by for densification.value_cols (
list[object]) – List of column names containing values to be preserved.fill_value – Value to use for filling missing time points (default: 0.0).
- Return type:
DataFrame- Returns:
Complete DataFrame with densified profiles indexed by grp_cols + [time_col].
- laurel.utils.plotting.get_band_bounds(qtls)[source]
Pair quantiles symmetrically around the median for band plotting.
Given a list of quantile levels that includes 0.5 and is of odd length, returns pairs
(low, high)ordered from the outermost band inward, ready forplot_quantile_bands().- Parameters:
qtls (
list[float] |Index) – Sorted list (orpd.Index) of quantile levels. Must contain the median (0.5) and have an odd number of entries.- Return type:
list[tuple[float,float]]- Returns:
List of
(lower_quantile, upper_quantile)tuples, outermost first.- Raises:
RuntimeError – If the median is absent or the length is even.
- laurel.utils.plotting.plot_quantile_bands(g, x, y_quants, palette='mako_r', **kwargs)[source]
Overlay shaded quantile bands on a seaborn FacetGrid.
For each pair of symmetric quantile columns (e.g. 5th/95th, 25th/75th), draws a
fill_betweenband using colours drawn frompalette. Bands are drawn from outermost to innermost so darker inner bands are rendered on top.- Parameters:
g (
FacetGrid) – SeabornFacetGridto draw onto.x (
str) – Column name for the x-axis (e.g."local_time").y_quants (
list) – Sorted list of quantile column names corresponding to the quantile levels (must be compatible withget_band_bounds()).palette (
str) – Seaborn palette name for colouring bands. Defaults to"mako_r".**kwargs – Additional keyword arguments forwarded to
plt.fill_between(e.g.alpha,linewidth).
- Return type:
FacetGrid- Returns:
The modified
FacetGrid.
laurel.utils.sensitivity module
Sensitivity analysis helpers using polynomial chaos expansion via OpenTURNS.
Provides SensitivityAnalysis for computing first- and total-order
Sobol’ indices from the N-SoW experimental design, and two helper functions
for converting between the lower-triangular correlation dictionary format used
in YAML configuration and the openturns.CorrelationMatrix format.
The Sobol’ indices quantify how much of the variance in a scalar output (e.g. peak substation load) is attributable to each input parameter, allowing researchers to rank the importance of adoption-rate, charging-power, and battery-reserve assumptions.
Key design decisions
Polynomial chaos metamodel: Sobol’ indices are estimated from a functional-chaos polynomial expansion of the model response rather than by direct Monte Carlo, because the N-sample budget is too small for reliable direct estimation. The metamodel approach also provides a validation metric (relative residual error) to flag poor fits.
Correlation dictionary format: The YAML-friendly lower-triangular dict (only off-diagonal entries listed, indexed by variable name) is converted to/from the
openturns.CorrelationMatrixobject so that the copula correlation structure can be defined in config files without numeric indexing.
- class laurel.utils.sensitivity.SensitivityAnalysis(scens, output_col, input_vars, builder)[source]
Bases:
objectPolynomial-chaos-based Sobol’ sensitivity analysis using OpenTURNS.
Wraps the OpenTURNS
FunctionalChaosAlgorithmworkflow:Convert scenario inputs and outputs to
ot.Sampleobjects.Fit a polynomial chaos expansion metamodel via
fit_metamodel().Extract first- and total-order Sobol’ indices via
calculate_sobols().Visualise results via
plot_sobols().
- Parameters:
scens (
DataFrame) – DataFrame of scenario results, one row per State of the World.output_col (
str) – Name of the scalar output column to analyse (e.g."peak_load_95th_kw").input_vars (
dict[str,dict]) – Dict mapping input variable names to their distribution specs (forwarded toScenarioBuilder.build_input_dist).builder (
ScenarioBuilder) –ScenarioBuilderinstance used to construct the joint input distribution.
- __init__(scens, output_col, input_vars, builder)[source]
Initialise OpenTURNS sample objects and build the joint input distribution.
- calculate_sobols()[source]
Compute first- and total-order Sobol’ indices from the fitted metamodel.
- Return type:
DataFrame- Returns:
DataFrame with columns
["Input Factor", "first", "total"], one row per input variable.- Raises:
AttributeError – If
fit_metamodel()has not been called first.
- fit_metamodel(verbose=True)[source]
Fit a functional-chaos polynomial expansion to the scenario input/output data.
Runs
ot.FunctionalChaosAlgorithmand stores the result asself.metamodel. Ifverbose=True, prints the residual and relative error and displays a validation scatter plot.- Parameters:
verbose (
bool) – IfTrue, print metamodel fit diagnostics and show a validation scatter plot. Defaults toTrue.- Return type:
None
- plot_sobols()[source]
Render a horizontal bar chart of first- and total-order Sobol’ indices.
- Return type:
FacetGrid- Returns:
Seaborn
FacetGridwith x-axis fixed to [0, 1].- Raises:
AttributeError – If
calculate_sobols()has not been called first.
- laurel.utils.sensitivity.correl_dict_to_matrix(correl_dict)[source]
Convert a lower-triangular correlation dict to an
openturns.CorrelationMatrix.The dict format is:
{ "var_b": {"var_a": 0.6}, "var_c": {"var_a": 0.3, "var_b": 0.5}, }
Only the lower triangle (and optionally either direction) needs to be provided; the function mirrors values to fill the upper triangle.
- Parameters:
correl_dict – Nested dict mapping row variable → column variable → correlation coefficient. Variable order is inferred from the top-level key order.
- Returns:
Tuple of
(ot.CorrelationMatrix, var_names)wherevar_namesis the list of variable names in matrix index order.- Raises:
KeyError – If any off-diagonal pair is missing from the dict in both directions.
- laurel.utils.sensitivity.correl_matrix_to_dict(corr_mat, var_names=None)[source]
Convert an
openturns.CorrelationMatrixto a lower-triangular correlation dict.- Parameters:
corr_mat – An
openturns.CorrelationMatrixobject.var_names – Optional list of variable name strings, in the same order as the matrix rows/columns. If
None, names default to"var_0","var_1", etc.
- Returns:
Lower-triangular dict in the format accepted by
correl_dict_to_matrix().
laurel.utils.time module
Time-zone conversion, local-time computation, and circular time statistics.
This module provides utilities for working with timestamped dwell and event data across multiple U.S. time zones. Because vehicle dwells are recorded in UTC but load profiles must be expressed in local time, accurate time-zone lookup and conversion are central to the pipeline.
Key functions:
calc_time_zones_from_hexes()— look up the IANA time-zone string for each row based on its H3 hexagon, usingtzfpyfor point-in-polygon queries.calc_local_time()— group rows by time zone and convert UTC timestamps to timezone-naive local times.calc_avg_time_of_day()— compute the circular mean of a time-of-day array (Numba JIT), correctly handling the midnight wrap-around.
Key design decisions
Unique-hex caching:
calc_time_zones_from_hexes()resolves time zones only for the unique set of hexagon IDs before merging back, avoiding redundanttzfpycalls for the many rows that share the same hex.Timezone-naive output:
_get_local_time_by_tz()strips the timezone info after conversion (dt.tz_localize(None)), so downstream pandas operations can compare timestamps without mixed-tz errors.Circular statistics:
calc_avg_time_of_day()uses the unit-circle projection (cos/sin → arctan2) to compute a mean that wraps correctly at midnight; the same projection yields a meaningful standard deviation via the angular residuals.
- laurel.utils.time.calc_avg_time_of_day(t, full_day)
Calculate the average time of day, dealing with midnight.
Note: When two times are diametrically opposed to each other, the default average is the greater of the two possible averages.
- Parameters:
t (
ndarray) – the array of times-of-dayfull_day (
float) – the maximum value that t takes before resetting to zero (e.g. 24 for 24 hours in a day)
- Return type:
float
Returns: float of average hour-of-day in t
- laurel.utils.time.calc_local_time(df, time_cols, local_cols, tz_col, sort_col=None, grp_cols=None)[source]
Add timezone-naive local-time columns to a DataFrame of UTC timestamps.
Groups rows by timezone (and optionally by additional group columns), then calls
_get_local_time_by_tz()on each group to convert the UTCtime_colsto their local equivalents. Optionally re-sorts each group bysort_colafter conversion.- Parameters:
df (
DataFrame) – DataFrame containing UTC timestamp columns and a timezone column.time_cols (
str|list[str]) – UTC timestamp column name(s) to convert.local_cols (
str|list[str]) – Output column name(s), in the same order astime_cols.tz_col (
str) – Column containing IANA timezone strings (e.g."tz").sort_col (
str) – If provided, rows within each group are sorted by this column after local-time conversion.grp_cols (
str|list[str]) – Additional column(s) to group by before the timezone grouping. Useful when each vehicle/region should be treated independently.
- Return type:
DataFrame- Returns:
dfwith new columns given bylocal_colscontaining timezone-naive local timestamps.- Raises:
RuntimeError – If
grp_colsis not a string, list, orNone.
- laurel.utils.time.calc_time_attrs(df, time_col, attrs)[source]
Add datetime accessor attributes as new columns (e.g.
hour,dayofweek).For each attribute name in
attrs, accessesdf[time_col].dt.<attr>and writes the result to a new column named{time_col}_{attr}.- Parameters:
df (
DataFrame) – DataFrame with a datetime column.time_col (
str) – Name of the datetime column to extract attributes from.attrs (
list[str]) – List ofpandas.DatetimeIndexaccessor attribute names (e.g.["hour", "dayofweek", "month"]).
- Return type:
DataFrame- Returns:
dfwith one new column per entry inattrs.
- laurel.utils.time.calc_time_zones_from_hexes(df, hex_col, tz_col='tz')[source]
Assign an IANA time-zone string to each row based on its H3 hexagon.
Converts hex IDs to centroid coordinates, queries
tzfpyfor the corresponding time zone, then merges the result back ontodf. To minimise expensive point-in-polygon queries, only the unique hex values are resolved; rows sharing the same hex reuse the cached result.- Parameters:
df (
DataFrame) – DataFrame containing a column of H3 integer cell IDs.hex_col (
str) – Name of the column holding H3 integer cell IDs.tz_col (
str) – Name of the output column to write the IANA timezone string into. Defaults to"tz".
- Return type:
DataFrame- Returns:
dfwith a newtz_colcolumn ofpd.Categoricaltimezone strings and the original index restored.
- laurel.utils.time.get_timezone_from_hex(hex)[source]
Return the IANA timezone string for the centroid of an H3 hexagon.
- Parameters:
hex (
int|str) – An H3 cell ID as either anumpy.uint64integer or a hex string.- Return type:
str- Returns:
IANA timezone string (e.g.
"America/Los_Angeles").- Raises:
RuntimeError – If
hexis neither an integer nor a string.
- laurel.utils.time.get_timezones(hexes, params)[source]
Kedro node wrapper: add a timezone column to a hexagon correspondence table.
- Parameters:
hexes (
DataFrame) – DataFrame with at least one column of H3 cell IDs.params (
dict) –Configuration dict with the following key:
hex_col (
str): Name of the H3 cell ID column.
- Return type:
DataFrame- Returns:
hexeswith a new"tz"column of IANA timezone strings.
- laurel.utils.time.get_total_time_units_filtered(start, end, unit, filterer=None)[source]
Count the number of time-unit boundaries between
startandend, optionally filtered.Generates a date range from
floor(start)toceil(end)atunitfrequency and counts the matching timestamps. The optionalfilterercallable allows arbitrary masks (e.g. keep only weekdays) to be applied before counting.- Parameters:
start – Observation start timestamp.
end – Observation end timestamp.
unit – Pandas offset string for the time unit (e.g.
"1h","1d").filterer – Optional callable that accepts a
Series[Timestamp]and returns a booleanSeries. OnlyTruetimestamps are counted.
- Returns:
Integer count of qualifying time-unit boundaries in the window.
- laurel.utils.time.total_hours(s)[source]
Convert a Series of timedeltas to fractional hours.
- Return type:
Series
- laurel.utils.time.total_time_units(s, unit)[source]
Convert a Series of timedeltas to fractional time units.
- Parameters:
s (
Series) – Series ofpd.Timedeltavalues.unit (
str) – Pandas offset string defining the unit (e.g."1h","1min").
- Return type:
Series- Returns:
Float Series of total elapsed units.