This workspace contains a rapid research prototype for local forecasting and edge orchestration of EV charging demand against on-site photovoltaic generation. It is the code release for a short paper submitted to the NextGCom 2026 EVOLVE special session.
- A small package, evloadcontrol/, that:
- discovers the EV charger columns present in the dataset and loads only the columns it needs (data.py),
- resamples raw telemetry into 15-minute intervals and engineers the lag/rolling/cyclical features (features.py),
- trains the XGBoost forecasters and the persistence/ridge baselines (models.py),
- implements the deterministic edge load-control decision, its persistence-forecast counterfactual, and the grid-limit sensitivity sweep (orchestrator.py),
- runs a moving block bootstrap to check whether XGBoost's edge over persistence survives temporal autocorrelation (significance.py),
- computes the temporal-error and per-charger-utilization breakdowns (analysis.py),
- plots the forecast traces and sensitivity curves (plotting.py),
- and optionally trains a PyTorch MLP/LSTM/1-D CNN comparison against the same held-out split (torch_models.py), plus a second LSTM given XGBoost's own engineered features so the raw-sequence models face no input asymmetry, retraining each configuration across 5 seeds and reporting mean ± std. This last step is a strictly optional add-on, never imported by default.
- Four notebooks in notebooks/, the recommended way to explore this project (see below).
- A thin CLI entry point in pipeline.py (
evloadcontrol/cli.pyunderneath) that runs the full pipeline end to end and writes metrics, plots, and model artifacts tooutputs/. It is useful for automation or a real edge deployment, and not required for exploring the results. - Dependencies managed as a uv project (
pyproject.toml+uv.lock).
Reproducibility: every stochastic step (XGBoost's row/column subsampling, the PyTorch initializations, the block bootstrap) uses the fixed seeds in evloadcontrol/config.py (SEEDS = (42, 43, 44, 45, 46)), so the CLI and notebooks reproduce the paper's tables and figures exactly, not just approximately.
Seeds and what the paper reports: every XGBoost number the paper prints is a mean over all five seeds, including the accuracy metrics, the feature ablation, the hourly error breakdown, the gain importances, and the Fig. 1 window errors. A few artifacts have no meaningful average and come instead from REFERENCE_SEED (42): the saved model files, the traces Fig. 1 plots, the paired predictions the block bootstrap resamples, and the controller event log, whose throttle counts are integers the paper reasons about individually. For those, summary.json also records the across-seed spread (throttle_seed_spread) so the reference run can be told apart from a seed artifact.
This repository does not include the raw telemetry CSV. The UMA Adabyron dataset is private and available on request from its maintainers, not redistributed here. To run the pipeline, place your own export (or a compatible CSV with the same column naming convention) at data/20260514_uma_adabyron_data.csv, or pass a different path via --data-path. The pipeline discovers whichever EV charger columns are present in the header and only loads the columns it needs, so it adapts automatically to datasets with a different charger count.
Install uv (brew install uv on macOS), then from this directory:
uv sync --extra notebooks --extra torchThat resolves and installs the core pipeline plus the notebook and PyTorch extras into a local .venv, without needing to create or activate one by hand. Leave off --extra torch if you don't need the deep-learning comparison (it is never required for the default pipeline).
notebooks/ is the easiest way to run this project: no CLI flags, each step's output is visible inline, and every notebook is pinned to the same fixed seeds as the paper, so re-running one reproduces the paper's numbers exactly.
uv run --extra notebooks jupyter lab notebooks/- 01_data_exploration.ipynb loads the raw telemetry, resamples it, and looks at the PV/EV signals and their correlations before any modeling.
- 02_forecasting_and_baselines.ipynb trains the XGBoost forecasters, compares them against persistence/ridge and the deep-learning architectures (Table I), runs the block bootstrap significance test (Section IV-C), and reproduces the ablation study (Table IV) and Fig. 1.
- 03_orchestrator_and_sensitivity.ipynb runs the deterministic orchestrator, logs individual throttle events with genuine-risk flags, reruns the orchestrator on persistence forecasts to quantify what the forecaster buys the control loop (Section IV-F), sweeps the safe grid-limit parameter, and breaks down forecast error by time of day and by charger.
- 04_deep_learning_comparison.ipynb trains the PyTorch MLP/LSTM/CNN comparison and compares it against XGBoost's 5-seed mean on the same basis (requires
--extra torch).
Each notebook is self-contained (it reloads and rebuilds whatever it needs), so they can be run independently and in any order. They import directly from the evloadcontrol package rather than duplicating logic.
The same pipeline as a single script, useful for a real deployment or a CI job rather than interactive exploration:
uv run python pipeline.py --data-path data/20260514_uma_adabyron_data.csvFor faster iteration, limit the rows during development:
uv run python pipeline.py --data-path data/20260514_uma_adabyron_data.csv --max-rows 50000To also run the PyTorch MLP/LSTM/CNN comparison against the same held-out split (requires the torch extra installed via uv sync --extra torch):
uv run python pipeline.py --data-path data/20260514_uma_adabyron_data.csv --include-torchThis is never required for the default pipeline. evloadcontrol.torch_models is only imported when --include-torch is passed, so a standard edge deployment does not need PyTorch installed.
For presentations, live_demo.py (evloadcontrol/live_demo.py
underneath) loads the already-trained models from a prior pipeline.py run
and replays a short window of the held-out test split one interval at a
time, single-sample inference (not a batch predict), timed and printed live
with a short pause between intervals so it reads as a real-time stream on
camera. By default it auto-selects a window centered on the largest genuine
throttle event, the same data-driven-window principle plot_forecast_zoom
uses, so it doesn't need a hand-picked date:
uv run python pipeline.py --data-path data/20260514_uma_adabyron_data.csv # once, to produce outputs/model_*.json
uv run python live_demo.pyUseful flags: --grid-limit (default 1000 W, the stress case with more
events to show), --window-intervals (how many 15-minute steps to replay),
--delay (seconds between intervals, for pacing on camera), --start (an
explicit index into the test split instead of auto-selection).
The pipeline writes the following files to outputs/:
model_pv.json,model_ev.jsonare the trained XGBoost models.feature_importance_pv.csv,feature_importance_ev.csvhold the per-feature gain importances as a mean and std across the 5 seeds. Gain shifts between near-duplicate features (current PV and its 15-minute lag) from seed to seed, so the per-feature std matters here as much as the mean.test_predictions.csvholds actual vs. predicted values for both targets on the held-out split.forecast_plot.pngis the forecast trace over the full test horizon (each day is a sliver at this scale, so it works as a coverage sanity check but not for reading forecast quality).forecast_plot_zoom.pngshows the same traces over a two-week window chosen for typical (not spike) EV demand, readable at daily resolution. This is the paper's Fig. 1.baseline_comparison.csvcompares XGBoost, persistence, and ridge regression by MAE/RMSE per target. XGBoost's row is a mean/std over 5 seeds (itssubsample/colsample_bytree< 1 makerandom_statea real variance source), while persistence and ridge are deterministic solvers with no seed dependence.bootstrap_significance.csvholds the moving block bootstrap (24h blocks, 10,000 resamples) 95% CI onMAE(XGBoost) - MAE(persistence)per target, i.e. whether XGBoost's point-estimate edge over persistence survives temporal autocorrelation in the 15-minute series (Section IV-C).baseline_comparison_with_torch.csvadds the PyTorch MLP/LSTM/CNN comparison and the engineered-feature LSTM variant to the above, each as mean/std MAE and RMSE over the same 5 seeds (only written with--include-torch).ablation_study.csvcompares the base (current-timestep only) feature set against the full engineered one, both sides averaged over the same 5 seeds so the ablation delta does not mix a mean with a single run.grid_sensitivity.csv,grid_sensitivity_plot.pnggive throttle rate and mean per-EV reduction swept across safe grid limits.hourly_error_breakdown.csvbuckets forecast MAE by time of day, seed-averaged, with the across-seed std per bucket.charger_utilization.csvreports per-charger total energy delivered and active-charging share over the full telemetry span.orchestrator_events_<grid_limit>w.csv,orchestrator_events_1000w.csvhold one row per triggered throttle event at the run's--grid-limitand at a fixed 1 kW stress case, including agenuine_riskflag computed from actual (not forecasted) PV/EV values. The default grid limit triggers too rarely to assess control quality from the aggregate rate alone, so these logs let each event be checked individually instead of only counted.summary.jsonis the consolidated run summary including all of the above, plus the persistence-forecast orchestrator counterfactual (throttle rate, raw decision-agreement rate, Cohen's kappa, and the agreement restricted to intervals where either controller acts, Section IV-F).
The workflow is designed for a short paper on edge-deployed load control, with a focus on low-latency local forecasting, deterministic orchestration, and battery-less microgrid constraints.
Released under the MIT License.