perf: scale the objective before handing the model to the solver#104
Draft
andig wants to merge 3 commits into
Draft
perf: scale the objective before handing the model to the solver#104andig wants to merge 3 commits into
andig wants to merge 3 commits into
Conversation
CBC judges improvements against absolute tolerances of about 1e-7. Prices are given in currency per Wh, so the raw objective coefficients land close to that bound: the peak attenuation tie-breakers sit around 1e-9 per W, and the cost terms are not far above. Improvements that small are indis- tinguishable from numerical noise, so the solver both explores a degenerate search tree and stops on solutions that are not optimal. Multiplying the assembled objective by a constant does not change its argmax, but it lifts the coefficients into a range the solver can resolve. The reported objective value is recalculated from the solution by get_clean_objective_value(), so the API response stays in the original unit and no expected test result changes because of the scaling itself. Measured over all test cases, each run with the stored strategy and with every charging strategy (14 cases x 5 settings, interleaved A/B, min of 7): total solve time 12.78 s -> 10.07 s (0.79x) worst case, none 1.31 s -> 0.79 s (0.60x) worst case, grid peaks 2.17 s -> 1.03 s (0.47x) No solution got economically worse, several got better. The largest gain is 021-min-pv-use-case, where the previous model returned a solution 0.59% below the optimum. Case 018-high-soc-initial got slower (0.13 s -> 0.23 s with attenuate_grid_peaks), which is branching luck rather than a trend. The new test case pins the recovered optimum: it fails without the scaling.
The peak attenuation strategies create one ramp variable per time step, but the ramp constraints only cover the steps that have a predecessor. The variable of the first step is therefore never constrained, it only carries the ramp penalty and is driven to zero. Index the ramps by the step they lead into instead, which removes one variable per leveled grid side without changing the model.
…ed case The scenario that proves the objective scaling is 021 solved without a charging strategy. Adding it as a data driven case duplicated 2839 lines of fixture that differ from the existing file in one field. Read the request from that file instead and assert the objective in a dedicated test module, as AGENTS.md suggests for behaviour the data driven harness cannot express. The assertion is a lower bound rather than an equality: a future model that finds an even better solution here should not fail this test. Also drops the duplicated rationale at the call site, the comment on OBJECTIVE_SCALE covers it.
Member
Author
andig
marked this pull request as draft
July 24, 2026 15:52
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.
The grid peak shaping work (#93, #96, #102) made the peak attenuation strategies noticeably slower: measured against the commit before them,
attenuate_grid_peakscosts about 1.3x median and up to 3x on single cases, while the default strategies are unaffected. Looking for the cause turned up something more general than the peak code.The problem
CBC judges improvements against absolute tolerances of about
1e-7. Prices enter the model in currency per Wh, so the raw objective coefficients land close to that bound. The peak attenuation tie-breakers are the extreme case at roughly1e-9per W, but the cost terms are not far above it either. Improvements that small are indistinguishable from numerical noise, so the solver explores a degenerate search tree and stops on solutions that are not actually optimal.The change
Multiply the assembled objective by a constant before handing it to the solver. This does not change the argmax, it only moves the coefficients into a range the solver can resolve.
get_clean_objective_value()recomputes the reported value from the solution, so the API response stays in the original unit and nothing in the contract changes.The second commit removes a ramp variable that the peak strategies create for the first time step but never constrain. It carries the ramp penalty, is driven to zero, and contributes nothing.
Measurements
All test cases, each run with its stored strategy and with every charging strategy (14 cases x 5 settings), old and new interleaved in one process, min of 7 runs:
noneattenuate_grid_peaksNo solution came out economically worse. Several came out better, the largest being
021-min-pv-use-case, where the previous model returned a solution 0.59% below the optimum.018-high-soc-initialgot slower (0.13 s to 0.23 s withattenuate_grid_peaks); that is branching luck on one instance, not a trend.The gain is not limited to the peak strategies, the default path benefits as much. It does not remove the extra work the peak strategies add to the model (one peak variable,
T - 1ramp variables and3T - 2constraints per leveled side), it makes the solver handle the whole model better.Test
tests/test_objective_scaling.pypins the recovered optimum. It reads the request from the existing021case, solves it without a charging strategy, and asserts a lower bound on the objective, so a future model that finds an even better solution there does not fail it. Onmainthe same request returns1.4243782673against a threshold of1.4327495658.