Daily Budget Weighting Math (Advanced)
This document explains the exact math behind:
- Unmanaged usage reserve (Advanced tab)
- Managed device flexibility (Advanced tab)
- Observed hourly peak caps (split-budget safety)
- Confidence (backtested forecast-skill score shown in the UI)
- Profile blend confidence (how quickly learned behavior influences the plan internally)
The formulas here match the current implementation in lib/dailyBudget.
1) Inputs used by the planner
For each local hour h (0-23), PELS works with:
D[h]: default profile weight (baseline day shape), normalized to sum to 1U[h]: learned uncontrolled weight, normalized to sum to 1C[h]: learned controlled weight, normalized to sum to 1s: learned controlled share of total energy (0..1)r: unmanaged reserve mode (0 = balanced,1 = conservative)w: internal managed-load floor/profile weight (current implementation:0.30)p: managed device price flexibility (0.30 = low,0.60 = medium,0.85 = high)Umax[h]: robust upper observed uncontrolled kWh envelope for local hourh(hourly quantile)Cmax[h]: robust upper observed controlled kWh envelope for local hourh(hourly quantile)Umin[h]: robust lower positive observed uncontrolled kWh envelope for local hourh(0means no minimum data)Cmin[h]: robust lower positive observed controlled kWh envelope for local hourh(0means no minimum data)m: observed-peak margin ratio (current implementation:0.20)W: observed-peak rolling window in days (current implementation:30)qMax: upper quantile for observed caps (current implementation:0.90)qMin: lower quantile for observed minimums (current implementation:0.25)Nq: minimum samples per hour before quantiles are used (current implementation:5)price[b]: combined price for each plan bucketpricePosition[b]: normalized position within the remaining price range (0 = cheapest,1 = most expensive)
2) How daily learning updates profiles
At day rollover, PELS splits each bucket into uncontrolled and controlled kWh (if controlled split data exists), then builds daily hour weights:
dayU[h] = hourlyUncontrolled[h] / totalUncontrolleddayC[h] = hourlyControlled[h] / totalControlled
Each profile is a running average:
nextWeight[h] = (prevWeight[h] * sampleCount + dayWeight[h]) / (sampleCount + 1)Controlled share is also a running average:
dayShare = totalControlled / (totalControlled + totalUncontrolled)
nextShare = (prevShare * sampleCount + dayShare) / (sampleCount + 1)Observed hourly peaks are updated at rollover too:
nextUmax[h] = quantile(hourlyUncontrolled[h], qMax) over buckets in last W days
nextCmax[h] = quantile(hourlyControlled[h], qMax) over buckets in last W daysObserved hourly minimums are updated from the same rolling window:
nextUmin[h] = quantile(hourlyUncontrolled[h] where > 0, qMin) over buckets in last W days
nextCmin[h] = quantile(hourlyControlled[h] where > 0, qMin) over buckets in last W daysFor low sample counts (count < Nq), PELS falls back to raw extrema for that hour:
nextUmax[h] = max(...)
nextCmax[h] = max(...)
nextUmin[h] = min(... where > 0)
nextCmin[h] = min(... where > 0)3) Managed-load profile weight (w) math
w is no longer exposed as a normal user setting. The Advanced tab exposes Unmanaged usage reserve instead; that mode affects unmanaged reserve floors, not the learned controlled/uncontrolled profile split.
Internally, w is fixed at the default value and scales the contribution of the controlled profile relative to uncontrolled, using learned controlled share s.
denom = (1 - s) + s * w
uncontrolledScale = (1 - s) / denom
controlledScale = (s * w) / denom
learnedUncontrolled[h] = U[h] * uncontrolledScale
learnedControlled[h] = C[h] * controlledScale
learnedCombined[h] = normalize(learnedUncontrolled[h] + learnedControlled[h])Internal behavior:
- lower
w: less controlled history influences the learned shape - higher
w: controlled contribution follows more of measured shares - current implementation: fixed default, so changing Unmanaged usage reserve does not reshape controlled history
Example A: managed-load profile weighting
Assume:
s = 0.40(40% controlled energy historically)w = 0.30(default)
Then:
denom = 0.60 + 0.40 * 0.30 = 0.72
uncontrolledScale = 0.60 / 0.72 = 0.8333
controlledScale = 0.12 / 0.72 = 0.1667So even though controlled energy share is 40%, the learned shape uses about 16.7% controlled influence internally.
4) Confidence
PELS has two distinct confidence concepts:
4a) Profile blend confidence (internal)
Profile blend confidence controls how quickly learned profiles replace the default profile in the planner. It is not shown in the UI.
profileBlendConfidence = clamp(profileSampleCount / 14, 0, 1)Profile blending applied by the planner:
effectiveUncontrolled[h] = D[h] * (1 - profileBlendConfidence) + learnedUncontrolled[h] * profileBlendConfidence
effectiveControlled[h] = learnedControlled[h] * profileBlendConfidence
combined[h] = normalize(effectiveUncontrolled[h] + effectiveControlled[h])Implications:
- Early days: plan stays close to default profile
- As profile blend confidence grows: learned behavior gradually takes over
- Controlled contribution ramps with profile blend confidence
4b) Budget confidence (UI-facing)
Budget confidence is a backtested forecast-skill score computed from the last 30 complete local days (excluding today and days overlapping unreliable periods). This is the value shown in the Budget tab.
It has two components:
Regularity score
Measures how consistent the home's daily usage shape is across history.
For each valid day i:
looCentroid = mean of all other days' normalized actual profiles
dayScore[i] = clamp(1 - L1(actualProfile[i], looCentroid) / 2, 0, 1)
regularityScore = mean(dayScores) * clamp(validActualDays / 14, 0, 1)Adaptability score
Measures how well the home follows shifted budget plans when controlled load exists. Only uses days with near-complete plan data (≥90% of hourly buckets).
For each valid planned day:
planFitScore = clamp(1 - L1(actualProfile, plannedProfile) / 2, 0, 1)
controlledShare = controlledDayKWh / totalDayKWh
shiftDemand = max(0.20, L1(plannedProfile, centroid) / 2)
scoreWeight = controlledShare * shiftDemand
adaptabilityScore = weightedMean(planFitScores, scoreWeights) * clamp(validPlannedDays / 14, 0, 1)validPlannedDays counts only planned days with positive scoreWeight, so low-evidence histories ramp slowly.
Combined confidence
weightedControlledShare = weightedMean(controlledShare, weights = shiftDemand)
adaptabilityInfluence = clamp(weightedControlledShare * 1.2, 0, 0.85)
confidence = regularityScore * (1 - adaptabilityInfluence)
+ adaptabilityScore * adaptabilityInfluenceIf there is no valid planned-day data or total day weight is zero, confidence falls back to regularity score alone.
This makes adaptability dominate only when the home historically has meaningful controlled share; otherwise confidence is mostly whole-home regularity.
A bootstrap confidence interval (5th/95th percentile, 500 iterations) is computed for debug/validation but is not shown in the UI.
Example B: profile blend confidence ramp
- After
3valid days: profile blend confidence is3/14 = 0.214 - After
10valid days: profile blend confidence is10/14 = 0.714 - After
14+valid days: profile blend confidence saturates at1.0
5) Observed hourly peak caps (split-budget safety)
This adds an hour-aware cap based on a robust upper observed split envelope for each local hour, plus margin. Observed bounds are recomputed from a rolling window, so old seasonal peaks eventually age out.
Per local hour:
Ucap[h] = Umax[h] * (1 + m)
Ccap[h] = Cmax[h] * (1 + m)PELS then combines the split caps into a total plausible-load cap:
observedCap[h] = Ucap[h] + Ccap[h]Notes:
- If one side has no usable observed max, the other side still contributes.
- If neither side has usable observed max, this cap is effectively disabled for that hour.
Final bucket cap is the minimum of:
- capacity per-hour cap (if configured), and
- total observed-peak cap above.
w does not affect the cap. Controlled usage is still available as flexible headroom above the floor even when w = 0.
5b) Observed hourly minimum floors (split-budget safety)
Observed minima create an hour-aware floor that prevents planning below typical historical usage for each side. Floors use robust lower positive envelopes and the same margin ratio m, but apply in the opposite direction:
Ufloor[h] = max(0, Umin[h] * (1 - m))
Cfloor[h] = max(0, Cmin[h] * (1 - m))The floor always includes the uncontrolled minimum. The Unmanaged usage reserve mode controls how defensively this unmanaged floor is reserved. Controlled minimums are added with the fixed internal managed-load floor weight w:
floor[h] = Ufloor[h] + w * Cfloor[h]Important behavior:
- Floors are enforced only while budget remains; if floors exceed remaining budget, all floors are scaled down proportionally to fit the budget.
- Balanced unmanaged reserve keeps the unmanaged floor closer to the learned minimum.
- Conservative unmanaged reserve raises the unmanaged floor toward the robust reserve envelope.
- Controlled service-floor influence stays fixed when changing unmanaged reserve mode.
- Controlled load between the floor and cap remains flexible budget headroom.
6) Managed device flexibility (p) math
Price shaping applies only when:
- price optimization is enabled
- daily price shaping is enabled
- complete remaining price data is available
Price range is computed from remaining buckets:
minPrice = min(remainingPrices)
maxPrice = max(remainingPrices)
priceRange = maxPrice - minPriceIf priceRange is zero or within the planner's near-flat deadband, price shaping is effectively disabled for that plan. Otherwise the selected managed-device flexibility maps to p, which is used directly as the effective shaping strength:
pEff = pThe planner first builds a neutral allocation from profile/history weights, floors, and caps. It then builds a full-flex price target between the same effective bounds:
pricePosition[b] = (price[b] - minPrice) / priceRange
priceTarget[b] = cap[b] - pricePosition[b] * (cap[b] - floor[b])priceTarget[b] - floor[b] becomes the preferred redistribution weight after floors are reserved. That means the cheapest bucket targets its cap, the most expensive bucket targets its floor, and intermediate buckets land between those extremes according to price.
The final plan blends neutral allocation and full-flex allocation:
planned[b] = neutralAllocation[b] * (1 - pEff)
+ fullFlexAllocation[b] * pEffBehavior:
- Low (
p = 0.30): modest price shaping - Medium (
p = 0.60): default price shaping - High (
p = 0.85): stronger movement toward cheaper feasible hours - Internally,
p = 1: cheapest remaining bucket is allowed up to cap, most expensive remaining bucket is held to floor when the remaining budget permits it - Values between 0 and 1 smoothly blend profile-driven pacing and full price-driven pacing
Example C: price flex effect
Assume three remaining buckets:
prices = [10, 20, 30]
caps = [4, 4, 4]
floors = [0, 0, 0]pricePosition = [0, 0.5, 1]
priceTarget = [4, 2, 0]With a 6 kWh remaining budget and p = 1, the full-flex allocation is:
[4, 2, 0]With the same setup and p = 0.5, the result is halfway between neutral allocation and this full-flex allocation.
7) Practical tuning guidance
- Keep defaults unless you have stable data and a clear tuning goal.
- Change one setting at a time and observe at least one full day.
- If unmanaged household load regularly causes budget misses, use Conservative unmanaged reserve.
- If too much budget is held back from managed devices, use Balanced unmanaged reserve.
- If plan movement by price is too aggressive, lower Managed device flexibility.
- If the budget cannot be fully allocated under capacity and historical caps, the Budget UI shows an allocation warning; lower the daily budget or raise the relevant capacity/load assumptions.
- If confidence stays low, verify regular power reporting and controlled/uncontrolled split data.
8) Debug fields to inspect
With debug logging enabled for daily budget, these fields are useful:
Profile & planner fields
profileSampleCountprofileSplitSampleCountprofileControlledShareprofileLearnedWeightsprofileEffectiveWeightspriceFactorarray (debug-only legacy price multiplier view)profileObservedMaxUncontrolledKWhprofileObservedMaxControlledKWhpriceSpreadFactoreffectivePriceShapingFlexSharestate.allocationPressure— requested vs planned budget and whether caps prevent full allocation
Budget confidence fields (in state.confidenceDebug)
confidenceRegularity— regularity score (0..1)confidenceAdaptability— adaptability score (0..1)confidenceAdaptabilityInfluence— weight of adaptability in combined score (0..0.85)confidenceWeightedControlledShare— controlled share weighted by shift-demandconfidenceValidActualDays— number of valid days used for regularityconfidenceValidPlannedDays— number of valid planned days used for adaptabilityconfidenceBootstrapLow— 5th percentile bootstrap interval (debug only)confidenceBootstrapHigh— 95th percentile bootstrap interval (debug only)profileBlendConfidence— internal profile blend confidence (sample-count ramp)
These values let you verify whether behavior is caused by profile learning, observed-peak caps, budget confidence scoring, or price shaping.