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 via cvxpy. 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 nan if 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 as a.

Return type:

float

Returns:

Similarity score in [0, 1], or nan if 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. Returns nan if 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 as a.

Return type:

float

Returns:

Wasserstein-1 distance (in units of bins), or nan if either input is all-zero.

Raises:

RuntimeError – If a and b have 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 DataFrame also accepts a Dask DataFrame where 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 expensive set_index that Dask requires for keyed joins on non-index columns.

class laurel.utils.data.IndexIntegerizer(int_col)[source]

Bases: object

Round-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)
__init__(int_col)[source]
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 to pandas.Categorical to 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.

DwellSet inputs are handled transparently: only the underlying DwellSet.data DataFrame is modified; the wrapper is reconstructed via copy_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 df with object columns replaced by pd.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 isin filters, 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 of keep_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): If True, keep rows where the column is not in value_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): If True, return only the columns named in filters (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 (int64 or float64).

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.int64 or np.float64.

Raises:

RuntimeError – If ser has 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 in right_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 active group_columns param sets) are also unioned into the target set.

Parameters:
  • merge_params (dict) – Mutable config dict with at least a keep_right_columns key 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 in right_df.

Return type:

dict

Returns:

The mutated merge_params dict with keep_right_columns narrowed 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 right onto left, preserving left’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 in params["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_partitions to avoid the expensive global set_index that a standard Dask merge would require. right is 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 from right (plus its index) to include in the output. Must contain at least one entry.

    • merge_kwargs (dict): Keyword arguments forwarded verbatim to pandas.DataFrame.merge (e.g. on, how).

Return type:

pd.DataFrame | dd.DataFrame

Returns:

Merged DataFrame with the same type and index as left and the selected columns from right appended.

Raises:
  • RuntimeError – If left is not a pandas or Dask DataFrame.

  • RuntimeError – If right is not a pandas DataFrame.

  • RuntimeError – If keep_right_columns is 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 onto left (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 left plus the columns from right.

Raises:

NotImplementedError – If more than two join columns are supplied.

laurel.utils.data.select_columns(df, params)[source]

Return df restricted to the columns listed in params["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 df as a list of NumPy arrays, along with corresponding field names and dtype objects. The returned triple can be passed directly to np.rec.fromarrays.

Parameters:
  • df (DataFrame) – DataFrame to convert.

  • index (bool) – If True, include the index as the first field(s); the field name is taken from the index label, or 'index' if unlabelled. Defaults to True.

  • 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 to None.

Return type:

tuple

Returns:

Three-tuple (arrays, names, formats) where arrays is a list of NumPy arrays (one per included index level then column), names is the corresponding list of field name strings, and formats is the list of dtype objects.

Raises:

ValueError – If a dtype_mapping entry is not a type, a numpy.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 DataFrame or a DwellSet whose data attribute may be a Dask DataFrame.

Return type:

pd.DataFrame | DwellSet

Returns:

A pandas DataFrame, or a DwellSet whose data attribute is a pandas DataFrame.

Raises:

NotImplementedError – If ddf is neither a Dask/pandas DataFrame nor a DwellSet.

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:

  1. get_events() — pandas entry point; adds duration seconds, groups by entity, calls the wrapper.

  2. get_events_wrapper() — bridges pandas group DataFrames to NumPy arrays for Numba.

  3. get_events_core() — Numba @njit kernel; 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_col rather 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 to

  • include_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_col

  • grp_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 (as pd.Timedelta) to bridge within an event.

Return type:

DataFrame

Returns:

grp with out_col updated 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:

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() and calc_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 of points.

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 Shapely Point geometries (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 is weight_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_col with a single geometry column (center_col) of weighted centroid points in the same projected CRS.

Raises:

RuntimeError – If gdf has 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:

Key design decisions

  • Dask compatibility: add_geometries() converts a Dask DataFrame to dask_geopandas via map_partitions so 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_col to either a centroid point or the full hexagon polygon, then attaches the resulting GeoSeries as the geometry column. Handles both pandas and Dask DataFrames; for Dask inputs the geometry is attached per-partition via map_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:4326 and a geometry column of Shapely objects.

Raises:
  • RuntimeError – If geom_type is neither "point" nor "polygon".

  • RuntimeError – If data is 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 corresp by region_col, unions the H3 cells in each group into a contiguous region shape via cells_to_poly(), and returns a GeoDataFrame indexed by region.

Parameters:
  • corresp (DataFrame) – DataFrame with at least hex_col and region_col columns.

  • 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 CRS EPSG: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 as lat).

  • res (int) – H3 resolution (0–15).

Return type:

ndarray

Returns:

1-D uint64 array of H3 cell IDs, same length as lat.

laurel.utils.h3.coords_to_cells_wrapper(part, lat_col, lng_col, res)[source]
Return type:

ndarray

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 via h3.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 to add_geometries() (hex_col, optionally geom_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 within distance rings, finds those neighbors that exist in hexes, and averages their rows in embs. 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 as hexes.

  • include_center (bool) – If True, include the hex itself when averaging its neighbors. Defaults to False (ring neighbors only).

  • distance (int) – Number of H3 grid rings to include as neighbors. distance=1 means 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 in hexes.

laurel.utils.hex_neighbors.get_ngbr_idxs(hex, hex_to_idx, include_center, distance)[source]

Return the row indices (within hexes) of the neighbors of hex.

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 the hexes array.

  • include_center (bool) – Whether to include hex itself.

  • distance (int) – Ring distance passed to get_ngbrs().

Return type:

list[uint64]

Returns:

List of integer indices into the hexes/embs arrays.

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) – If True, include hex in the returned set.

  • distance (int) – Number of grid rings. distance=1 returns the 6 adjacent hexes (or 7 with include_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 a sjoin_nearest against the substation GeoDataFrame, and renames columns according to params["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 in hexes.

    • substation_col_renamer (dict[str, str]): Mapping from output column names to the corresponding column names in infra (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 as infra.

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: object

Evaluate 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(), the summary method 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.

__init__(dwell_locs, loc_groups)[source]
set_cutoff(cutoff)[source]

Update the cutoff and refresh the exceedance mask.

Return type:

None

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 its obs_uniform_ratio exceeds the cutoff.

  • "overall" — scalar fraction of locations exceeding the cutoff across the whole dataset.

  • "group" — per-group DataFrame with columns exceeds_cutoff, group_size, uniform_dwell_rate, group_frac, and exceeds_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 kind is not one of the three supported values.

summary_group()[source]
Return type:

DataFrame

summary_overall()[source]
Return type:

float

summary_raw()[source]
Return type:

Series

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: object

Context 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 with block.

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
__init__(level=50)[source]

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 via folium (through geopandas.explore). If trajectories=True, a second layer of LineString segments 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 least hex_col and hue_col present.

  • hue_col (str) – Column used to colour hexagons (e.g. "charging_mode"). Categorical columns are cast to str before plotting.

  • hex_col (str) – Column of H3 cell IDs (integer or string format both accepted).

  • trajectories (bool) – If True (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.Map object 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:

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 of leaves does not matter.

  • src_digits (int) – Not currently used; reserved for future digit-normalisation.

  • fill_leaf (int | None) – If not None, 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_leaf is None.

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:

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

  2. Read pass: re-scan the temporary PBF with location resolution (with_locations()) and the GeoInterfaceFilter to 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: object

osmium handler that retains OSM objects whose tag value matches a regex.

Implements the osmium.BaseHandler interface (node, way, relation methods) so it can be chained via osmium.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.

__init__(tag, pattern)[source]

Compile the regex and store the tag key.

node(n)[source]
relation(n)[source]
way(n)[source]
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. RegexTagFilter instances) applied to both passes.

  • tags (list[str]) – List of OSM tag keys to include as columns in the output GeoDataFrame (forwarded to GeoInterfaceFilter).

  • 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.FileProcessor with filters and optional geometry.

Parameters:
  • pbf_path (Path) – Path to the input OSM PBF file.

  • filters (list[BaseHandler]) – List of osmium.BaseHandler-compatible filter objects applied in order via with_filter().

  • with_locations – If True, attaches a location index (needed to resolve way/relation node coordinates) and appends a GeoInterfaceFilter.

  • **geo_int_kwargs – Additional keyword arguments forwarded to osmium.filter.GeoInterfaceFilter (e.g. tags).

Return type:

FileProcessor

Returns:

Configured osmium.FileProcessor ready 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 special random_seed pattern 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 of d.

  • 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 kedro

  • key_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; pass None for 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 params and adds the corresponding column to entities according to one of three patterns:

  1. Merge-type — if the value is a dict with keys id_columns and values, builds a lookup table via build_df_from_dict() and left-joins it onto entities. Raises if any entity is unmatched.

  2. Random seed — if the key is "random_seed", computes entity[seed_id_col] + master_seed so every entity has an independent but deterministic seed.

  3. Scalar — any other value is flattened (nested dicts become "parent_child" column names) and broadcast uniformly.

DwellSet inputs are handled transparently: the underlying DataFrame is modified and wrapped back into the original DwellSet.

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 (see build_df_from_dict()).

    • A dict {"seed_id_col": str, "master_seed": int} when the key is "random_seed".

Return type:

DataFrame | DwellSet

Returns:

entities with one new column per key in params.

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 that matplotlib/seaborn plots render without gaps.

  • plot_quantile_bands() — overlays shaded confidence bands on a seaborn.FacetGrid for a set of quantile columns symmetric around the median.

class laurel.utils.plotting.ProfileDensifier(time_col, freq, dur='1d')[source]

Bases: object

Class 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 for plot_quantile_bands().

Parameters:

qtls (list[float] | Index) – Sorted list (or pd.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_between band using colours drawn from palette. Bands are drawn from outermost to innermost so darker inner bands are rendered on top.

Parameters:
  • g (FacetGrid) – Seaborn FacetGrid to 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 with get_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.CorrelationMatrix object 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: object

Polynomial-chaos-based Sobol’ sensitivity analysis using OpenTURNS.

Wraps the OpenTURNS FunctionalChaosAlgorithm workflow:

  1. Convert scenario inputs and outputs to ot.Sample objects.

  2. Fit a polynomial chaos expansion metamodel via fit_metamodel().

  3. Extract first- and total-order Sobol’ indices via calculate_sobols().

  4. 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 to ScenarioBuilder.build_input_dist).

  • builder (ScenarioBuilder) – ScenarioBuilder instance 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.FunctionalChaosAlgorithm and stores the result as self.metamodel. If verbose=True, prints the residual and relative error and displays a validation scatter plot.

Parameters:

verbose (bool) – If True, print metamodel fit diagnostics and show a validation scatter plot. Defaults to True.

Return type:

None

plot_sobols()[source]

Render a horizontal bar chart of first- and total-order Sobol’ indices.

Return type:

FacetGrid

Returns:

Seaborn FacetGrid with 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) where var_names is 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.CorrelationMatrix to a lower-triangular correlation dict.

Parameters:
  • corr_mat – An openturns.CorrelationMatrix object.

  • 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, using tzfpy for 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 redundant tzfpy calls 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-day

  • full_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 UTC time_cols to their local equivalents. Optionally re-sorts each group by sort_col after 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 as time_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:

df with new columns given by local_cols containing timezone-naive local timestamps.

Raises:

RuntimeError – If grp_cols is not a string, list, or None.

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, accesses df[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 of pandas.DatetimeIndex accessor attribute names (e.g. ["hour", "dayofweek", "month"]).

Return type:

DataFrame

Returns:

df with one new column per entry in attrs.

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 tzfpy for the corresponding time zone, then merges the result back onto df. 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:

df with a new tz_col column of pd.Categorical timezone 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 a numpy.uint64 integer or a hex string.

Return type:

str

Returns:

IANA timezone string (e.g. "America/Los_Angeles").

Raises:

RuntimeError – If hex is 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:

hexes with 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 start and end, optionally filtered.

Generates a date range from floor(start) to ceil(end) at unit frequency and counts the matching timestamps. The optional filterer callable 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 boolean Series. Only True timestamps 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 of pd.Timedelta values.

  • unit (str) – Pandas offset string defining the unit (e.g. "1h", "1min").

Return type:

Series

Returns:

Float Series of total elapsed units.

Module contents