Skip to content

Refuse a schedule whose binaries came back off the integers - #136

Merged
andig merged 1 commit into
perf/two-stage-solvefrom
fix/preference-stage-integrality
Aug 7, 2026
Merged

Refuse a schedule whose binaries came back off the integers#136
andig merged 1 commit into
perf/two-stage-solvefrom
fix/preference-stage-integrality

Conversation

@andig

@andig andig commented Aug 7, 2026

Copy link
Copy Markdown
Member

Fixes the defect behind #127: an EV with c_min=4140 charging at a fifth of that in 14 of 192 slots, z_c sitting between 0.12 and 0.36, reported as Feasible.

What happens

_solve_preferences decides whether to keep its result by reading the variables back. A solve that runs out of clock before it finds an integer solution leaves the relaxation in them, and pulp reads that like any other result. It scores better on the preferences than any real schedule can — because it is one the model forbids — so improved is true, the first stage schedule is discarded, and problem.status is then forced to Optimal.

Reproduces on the split path at the default probe budget:

path=split  preference_stage=Not Solved  status=Feasible  5.70s
  slot   3: c=  615.46 gate= 1035.00  z_c=0.35678659
  ... 14 slots between 0.12 and 0.36

What changes

The acceptance test. Integrality joins the two conditions already checked there, for the reason the comment beside them gives: a solver that reports the wrong status must not be able to spend money, and it must not be able to hand back a schedule the model does not allow either. The stage now ends as Not Solved, kept the first stage and the first stage schedule survives.

A check before the result is built. Standing in for every stage above deciding correctly. A schedule that breaks the model is worse than no schedule — it looks like an answer and the caller charges a battery by it — so a fractional point is reported as no schedule rather than returned. One pass over the integer variables against a solve measured in seconds. It logs when it fires; a silent fallback is how this stayed invisible.

Note for anyone reading the check: pulp stores a binary as an integer bounded to 0 and 1, so LpBinary never survives on a variable and LpInteger is what the filter has to match. Getting that wrong makes the guard pass vacuously.

Tests

tests/test_fractional_solution.py, three cases, no captured request and no dependence on a timing race:

  • a preference stage that returns without an integer solution is refused, first stage schedule intact
  • a fractional point that reaches the result is reported as Not Solved with no schedule
  • values inside CBC's own integer tolerance still pass, so the guard does not fire on rounding

Full suite passes (115).

Base

Branched off perf/two-stage-solve rather than main, since that is where _solve_preferences lives. #135 picks it up on its next merge. Worth deploying independently of the perf work — production currently runs the rollup, where this is reachable.

Related: the probe change measured alongside this hides the bug by keeping most requests on the joint path. It does not remove it, and this fix stands on its own.

The second stage decides whether to keep its result by reading the variables
back, and a solve that ran out of clock before it found an integer solution
leaves the relaxation in them. That point scores better on the preferences than
any real schedule can, because it is one the model forbids: the binaries land
between 0 and 1 and every rule they gate stops holding. So the tie break kept
it, overwrote the first stage schedule, and the response went out with the
charge power gate ignored in a third of its slots and a status of Feasible.

Reported in #127, where an EV with c_min=4140 charged at a fifth of that in 14
of 192 slots, z_c sitting between 0.12 and 0.36. It reproduces on the split
path whenever the preference stage finds nothing before its slice runs out.

Integrality is now checked where the other two acceptance conditions already
are, for the reason stated there: a solver that reports the wrong status must
not be able to spend money, and it must not be able to hand back a schedule the
model does not allow either.

A second check sits right before the result is built, standing in for every
stage above deciding correctly. A schedule that breaks the model is worse than
no schedule, since it looks like an answer and the caller charges a battery by
it, so a fractional point is reported as no schedule at all rather than
returned. One pass over the integer variables against a solve measured in
seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@andig

andig commented Aug 7, 2026

Copy link
Copy Markdown
Member Author

@ekkea on top of the 2-stage solve. Merging since that hasn't hit master yet.

@andig
andig merged commit 8f75a4e into perf/two-stage-solve Aug 7, 2026
@andig
andig deleted the fix/preference-stage-integrality branch August 7, 2026 11:53
andig added a commit that referenced this pull request Aug 8, 2026
The second stage decides whether to keep its result by reading the variables
back, and a solve that ran out of clock before it found an integer solution
leaves the relaxation in them. That point scores better on the preferences than
any real schedule can, because it is one the model forbids: the binaries land
between 0 and 1 and every rule they gate stops holding. So the tie break kept
it, overwrote the first stage schedule, and the response went out with the
charge power gate ignored in a third of its slots and a status of Feasible.

Reported in #127, where an EV with c_min=4140 charged at a fifth of that in 14
of 192 slots, z_c sitting between 0.12 and 0.36. It reproduces on the split
path whenever the preference stage finds nothing before its slice runs out.

Integrality is now checked where the other two acceptance conditions already
are, for the reason stated there: a solver that reports the wrong status must
not be able to spend money, and it must not be able to hand back a schedule the
model does not allow either.

A second check sits right before the result is built, standing in for every
stage above deciding correctly. A schedule that breaks the model is worse than
no schedule, since it looks like an answer and the caller charges a battery by
it, so a fractional point is reported as no schedule at all rather than
returned. One pass over the integer variables against a solve measured in
seconds.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant