Level the grid profile against a free level instead of its peak and ramp - #130
Open
andig wants to merge 2 commits into
Open
Level the grid profile against a free level instead of its peak and ramp#130andig wants to merge 2 commits into
andig wants to merge 2 commits into
Conversation
The peak attenuation strategies priced the horizon maximum and the step to step ramp. Neither prices a single time step against the rest: the maximum is one value, and every path that rises monotonically between two levels carries the same ramp sum. Where the maximum is pinned by a load the schedule cannot touch, the cheapest schedule is therefore to charge flat out against it until the goal is met and then stop, which is the opposite of what the strategies promise. Penalize the distance of every step from a level the optimizer places freely, the linear stand in for the mean square deviation from constant grid power the strategies are meant to minimize. The weight sits a decade below peak leveling and a decade above ramp leveling, so lowering the peak still wins a direct trade, and the term is weighted by step length over the horizon so the same day carries the same weight however finely it is sampled. Level and distance are bounded by the power the side can physically carry, which keeps the new rows out of big M territory - unbounded they cost the largest stored case a factor of ten with the objective handed over unscaled. The bound falls back to the big M where a limit on the opposite side can feed this one past its natural cap, the same case the flow direction constraints exclude. Measured over 486 pinned peak cases: the profile changes in 35 % of them, mean deviation 11 % lower and up to 22 % on the worst, the peak never moves and the bill stays put to 6e-8 currency. Every stored schedule is unchanged except the case added here. Solve time is flat on the corpus as stored and up to 45 % higher when every case is forced onto a peak strategy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The ramp term priced the transitions of the grid profile, the deviation term added in the previous commit prices every step against a free level. Carrying both put two absolute value systems on the same grid power, and that overlap, not the extra rows, is what cost the solve time: on 018-high-soc-initial the simplex iterations go 419 with either term alone to 1299 with both, while the node count falls from 10 to 3. Removing it takes the forced attenuate_grid_peaks corpus from 5.456 s to 3.769 s (-31 %), back to parity with main at 3.911 s, and the model back to main's size - the deviation term covers T steps where the ramp covered T - 1, so the whole change is +2 rows and +2 columns per leveled side. The pinned peak sweep the term was written for is untouched: all 486 cases solve to the identical schedule with and without the ramp, and against main the deviation is 18 % lower with the peak never raised. What it gives up is shape ordering where the free level settles at zero. There the term is the energy through the side, which the energy balance already fixes, so a plateau, a jagged profile and a single spike of the same energy score alike. On synthetic horizons the import side deviation rises 6 % at 48 steps and 20 % at 192. Both halves are pinned by tests and the blind spot is documented where the constraints are built. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Follow-up to the question whether the peak shaving strategies carry the right incentive, i.e. minimum mean square deviation from constant grid import or export. They are close, but they miss one thing, and it is not a corner case.
attenuate_*_peakspriced two quantities: the horizon maximum, and the step to step ramp. Neither prices one time step against the rest.Where the maximum is pinned, the cheapest schedule is then to charge flat out against it until the goal is met and stop - the opposite of what the strategy promises. A 6 kW spike in step 2, an EV that needs 12 kWh:
Same peak, same energy, same bill.
The change
Two commits. The first adds a third term per levelled side, the second removes the ramp, which measurement showed the new term makes redundant on every case the strategies are aimed at.
Deviation from a free level
p_dev[t] >= |p_grid[t] - p_lvl|, withp_lvlplaced freely by the optimizer. That is the linear stand-in for the mean square deviation the strategies are meant to minimize - a square cannot go into a MILP, and a mean absolute deviation around a free level is the closest thing that can.penalty_base * 1e-4, a decade below peak levelling, so lowering the peak still wins a direct tradetest_objective_scalingexercises). The bound falls back to the big M where a limit on the opposite side can feed this one past its natural cap, mirroring the rule the flow direction constraints already useDropping the ramp
Carrying both put two absolute value systems on the same
p_grid. That overlap, not the extra rows, is what cost the solve time. On 018-high-soc-initial, withattenuate_grid_peaksforced:Iterations triple only when both are present - 419 with one, 464 with the other, 1299 together. Note the node count falls when the deviation term is added: it breaks objective ties, so the branch and bound search gets easier. The cost was never the search, and it was never the Python side either - 98 % of the added time was inside CBC, 2 % in pulp building the model.
Benchmarks
uv run python tools/bench_peak_leveling.py [--rev main] [--strategy ...] [--sweep]runs the stored cases, or the pinned peak sweep, through the working tree and a reference revision.All numbers: Apple M4, native CBC 2.10.13, best of 3 solves per case. Repeat runs vary by a few percent on the totals; the outlier runs are not reported here but the ordering was stable across all of them.
Solve time
attenuate_grid_peaks, 20 casesStored corpus with the strategies the cases ship with: 2.555 s -> 2.546 s. Only 5 of 20 cases level anything; the other 15 build an identical model.
Horizon scaling, synthetic day, PV bell, day/night tariff, EV goal at the end, both sides levelled:
Model size
Per levelled side, against main: +2 rows and +2 columns, total, not per step. The deviation term covers T steps where the ramp covered T - 1, so the two nearly cancel. 026 goes 117 -> 121 rows and 89 -> 93 columns; the synthetic 192 step horizon 2877 -> 2881 and 2113 -> 2117.
Schedules
Stored corpus: no schedule changes except the case added here.
486 pinned peak cases, now shipped as
--sweepso the numbers are reproducible (spike size x position x goal x charge power x base load = 3 x 6 x 3 x 3 x 3):Against the ramp version: 0 of 486 profiles differ. On the case class the strategies exist for, the ramp was contributing nothing.
What dropping the ramp gives up
Stated plainly, because it is a real regression and the corpus above does not show it.
The level is free, and on a side that mostly rests at zero it settles at zero. With
p_grid >= 0the term is thensum p_grid[t] * dt[t]- the energy through that side, which the energy balance already fixes. So it scores these identically, at 750 each:1500 1500 1500 1500 0 0 0 03000 0 3000 0 0 0 0 06000 0 0 0 0 0 0 0Below the peak, nothing orders them any more. The ramp did. The level sits at zero in 28 of 40 side-cases in the stored corpus, so this is the common case, not a corner.
Measured cost of that:
The 192 step figure is the honest headline against it. It was judged worth 31 % of the solve time; if that reads differently to you, reverting the second commit restores it and the first commit stands on its own.
Also here
test_cases/028-attenuate-peaks-below-a-pinned-peak.json, strict, the profile abovetests/test_peak_leveling.py: weight order, sampling independence, the bounds and their big-M fallback, the pinned peak regression, a guard against the ramp being reintroduced unnoticed, and the blind spot above pinned as an explicit test so a future change to the term has to confront it--sweepadded to the benchmark toolVerification
uv run pytestgreen, 111 tests.uv run ruff checkclean. Local runs used a native CBC 2.10.13 (the CBC that ships with pulp is x86-64 and does not run on Apple silicon); the container pins 2.10.10, so CI is the authority on the stored objective values.One merge order note: the reported
objective_valueof the new case carries theget_clean_objective_valueoff-by-one that subtractss[0]instead ofs_initial. When that fix lands, this expected value needs regenerating - the schedule itself is unaffected.🤖 Generated with Claude Code