Three runtime tiers, three external services
GridPulse runs as three separate things, and keeping them separate is the single most load-bearing decision in the system.
- A stateless web service serves the dashboard and the public JSON API. It reads from Redis and nothing else.
- An hourly scoring job fetches demand and weather, loads the current models from cloud storage, and writes forecasts, alerts and diagnostics into Redis.
- A daily training job retrains all three model families, validates each candidate, and persists the accepted ones.
Data comes from three outside services: the EIA API for demand, generation and interchange; Open-Meteo for 17 weather variables; and NOAA’s National Weather Service for severe-weather alerts. Forecast-hour weather is a composite of NOAA’s National Blend of Models over the base forecast — that arrives through Open-Meteo as a model option, not as a fourth feed of its own.
Why the web tier never does heavy work in the request path
The web container has no trained models on disk, no model metadata, and no path to the model store. That is deliberate, and it is enforced rather than merely intended: any component that reaches for model output must read it from Redis, because the scoring job is the only writer.
The rule exists because breaking it fails quietly. A function that reads a model from local disk does not crash in production when the disk is empty — it falls back to a simulated path and returns a plausible-looking series. Two real bugs shipped that way in a single day: an overview chart rendering noisy historical actuals at forward timestamps on every page load, and a model badge reporting “simulated” while the forecasts in Redis were real. Both looked fine.
So the fallbacks are now gated: when the web tier is configured for Redis-only operation and no real data is available, it returns nothing and the interface says it is warming up. That shows worse in a demo and behaves better in production.
What goes into a forecast: 49 features from 17 weather variables
Each region’s feature matrix carries 49 features — 17 raw weather variables and 32 derived from them. The derived set includes cooling and heating degree days against a 65°F baseline, wind power density, solar capacity factor, and a family of lag and rolling-window statistics over recent demand.
Every feature is backward-looking. Nothing in the matrix can see a value from after the timestamp it describes, which is what makes the holdout numbers below mean anything.
Where the weather is sampled
A balancing authority responds to weather across its whole territory, but the fetch originally sampled one representative point — for MISO, a single spot in rural Illinois standing in for a territory running from Minnesota to Louisiana. Each footprint is now sampled at up to 12 fixed cells and averaged, worth +1.14 sMAPE points overall (sMAPE is a symmetric variant of the average-error percentage; MISO +1.77, PJM +1.41) and approximately nothing for compact single-metro authorities, which keep their single point.
Two details matter more than they look. Wind direction uses a circular mean, because averaging 350° and 10° arithmetically points the wind due south. And the aggregation is unweighted, because population weighting was measured and added nothing — so production carries coordinates and needs no census data at runtime.
Three base models and one ensemble
XGBoost learns the non-linear weather-to-demand relationship from the full engineered feature set and is the best base model for 42 of 51 balancing authorities. Prophet fits additive seasonality with weather regressors. SARIMAX captures stationary structure, with its order selected automatically.
Why inverse-MAPE weighting instead of stacking
The ensemble gives each model a share of the vote in proportion to
how accurate it has been: weight ∝ (1/MAPE)³,
normalised to sum to one. MAPE is mean absolute percentage error
— the average miss, as a share of actual demand — so a
model with lower error carries more weight. Training a second model
to combine the three (“stacking”) would be the more
fashionable choice; this was picked because the weights are
transparent and trivial to debug — you can always answer
“which model is dominating right now, and why” by
reading three numbers.
The cubing is not decoration. Plain inverse-MAPE blending kept 15–30% of the weight on models running three to five times worse than the leader. The exponent makes the blend follow the best recent model and mix meaningfully only when its peers are genuinely close.
What the ensemble actually buys — and where it costs accuracy
GridPulse used to say the ensemble bought variance reduction in the tail. That claim is withdrawn. It rested on one balancing authority improving from 38.63% to 13.61%. On the current run that same authority is 13.68% under XGBoost alone and 14.72% under the ensemble — the blend is worse there.
More broadly, the served ensemble trails best-base in aggregate: 4.35% median per-BA error against 3.69%, and a 90th percentile of 14.27% against XGBoost-alone’s 9.87%. It wins on 21 of 51 balancing authorities.
The honest statement is that blending buys error decorrelation on some authorities and costs accuracy on others. It is a hedge, not a guarantee, and the per-authority table is the only reliable guide.
One further correction worth stating plainly: ensemble figures published before August 2026 were optimistically biased. The weights were fitted on the same window the blend was scored against. Re-scoring strictly out-of-sample moved the median from 3.89% to 4.35% and made it worse on 33 of 51 authorities. Base-model numbers were never affected.
How accuracy is measured: recursive, 168 hours, per authority
Why recursive scoring runs about twice the usual number
Most dashboards quote teacher-forced error: the model predicts one step ahead, then receives the true value before predicting the next. That measures a nowcast. A seven-day forecast has no true values to receive — the model’s own predictions feed forward as its lag features, and errors compound.
GridPulse scores the way it serves, recursively over a 168-hour holdout — a stretch of real history withheld from training, so the model is graded on hours it has never seen. That runs roughly 2× harder on the models than the teacher-forced equivalent. It is the number a seven-day forecast deserves.
Why there is no single “across-51” number
Accuracy is quoted per balancing authority, never pooled. The distribution is too wide for a mean to describe: on the best-achievable-per-authority view the best sits at 1.72% and the worst at 23.26%. (Those two bracket the best base model per authority. The 4.35% median quoted elsewhere on this site is a different figure — the served ensemble, across all 51 — and the gap between the two is the subject of the section above.) A single headline figure would be a different claim depending on which authority a reader cared about — which is to say, not a claim at all.
Every forecast is also scored against the balancing authority’s own published day-ahead forecast, on the same hours and the same settled actuals, with wins and losses both published.
The gates a model passes before it serves
The serve-path acceptance gate
Retraining daily means drawing a new model daily, and the draws are not all good. Replaying every persisted vintage for one authority showed about 27% produce recursive forecasts that collapse overnight demand into a phantom regime — a failure the published holdout cannot see, because it scores a fresh model on sliced historical rows rather than running the deployed candidate through the recursion path production actually uses.
So before the training job promotes a candidate, it replays that candidate through the real serve path from three anchors. A candidate that fails is still kept for forensics, but the pointer stays on yesterday’s accepted model. Serving a slightly older model that behaves is better than serving a new one that does not, which is the same rule the data layer follows when an upstream API degrades.
The visibility gate, and why it asks the most generous question
A second gate decides which authorities appear in the product at all. It is judged on the training holdout against the seven-day band — deliberately the most generous question available, because hiding a balancing authority is a heavy act. As measured on 2026-07-28, it hides none of the 51.
The cost of that generosity is real, and worth stating: one authority sat at 6.96% on the holdout, well inside the bar, while every served model graded a rollback at 24 hours on the live path. Both numbers were right about their own question, and nothing compared them. A second opinion is now computed every hour, published beside the verdict, and logged whenever the two disagree.
Where the forecast stops being a forecast
The scoring job produces a 30-day hourly demand forecast, but real weather forecast data only covers the first 16 days. Past that boundary the weather inputs are driven from a per-authority seasonal normal — a trailing ten-year “normal weather year” — with the current weather regime decaying into that normal over about five days.
The Forecast tab draws a visible divider at day 16 and labels both sides. Demand past that line reflects seasonal and daily patterns, not forward-looking signal, and the interface says so rather than drawing one continuous confident line. Atmospheric chaos limits deterministic forecast skill to roughly 10–14 days regardless of model, so this is the correct answer past that coverage rather than a cost-driven shortcut.
What this does not do
- It does not detect drift between trainings. Ensemble weights come from holdout error computed at training time, so they stay frozen for a day. If one model degrades on live actuals while another holds steady, nothing notices until the next retrain.
- It does not price energy or recommend trades. There is a merit-order cost model for context, not for decisions.
- It does not cover Alaska or Hawaii, and it covers 51 of the 63 balancing authorities that report to EIA — which is roughly 100% of lower-48 demand, because the missing ones are small.
- Its per-authority champion is not stable. Which base model wins for a given authority changes between retrains, so a specific claim about one is only true on the run that produced it.
Every number on this page traces to CANONICAL_FACTS.md, and the architecture decisions behind it — including the ones that turned out to be wrong — are in PRD.md.