Skip to content

Break near-tie plan comparison toward the less fragmented plan#4301

Open
springfall2008 wants to merge 1 commit into
mainfrom
feat/plan-fragmentation-tiebreak
Open

Break near-tie plan comparison toward the less fragmented plan#4301
springfall2008 wants to merge 1 commit into
mainfrom
feat/plan-fragmentation-tiebreak

Conversation

@springfall2008

Copy link
Copy Markdown
Owner

Problem

metric_min_improvement_plan (default 2.0p) keeps the incumbent plan whenever a freshly optimised plan is not better than it by that margin. This is deliberate anti-jitter hysteresis, but it has a side effect: under flat export rates, a fragmented (split) export staircase and a clean single export block are near cost-equal, so once a split plan forms it gets locked in permanently — every 5-minute recompute finds the cleaner merged block but discards it because the improvement is under the threshold.

Observed on a real system: evening export shown as a staircase (20:50–22:30 @28% · gap · 22:50–23:30 @7% · gap · 23:45 @6%) instead of a single 20:50–23:30 @0% block. Log confirmed:

Previous plan best metric is -224.78 and new plan best metric is -226.57
New plan metric is not significantly better (metric_min_improvement_plan 2.0), using previous plan

The merged plan was 1.79p better but under the 2.0p floor, so the split persisted.

Change

Two pure helpers on the Plan class, wired into the plan-comparison branch of calculate_plan:

  • plan_fragmentation(...) — counts contiguous active segments (battery discharge where export limit < 99, or charge above reserve). Adjacent same-mode slots merge into one segment; an idle time gap or a charge↔export mode change starts a new one. Fewer = cleaner.
  • should_replace_plan(metric_prev, metric_new, frag_prev, frag_new):
    • improvement >= metric_min_improvement_plan → adopt new (unchanged behaviour)
    • near-tie and improvement >= 0 and frag_new < frag_prev → adopt new (cleaner, never costlier)
    • otherwise keep previous

The improvement >= 0 guard means a cleaner plan is never adopted if it costs anything more.

Why it can't thrash

The tie-break only ever moves toward fewer segments inside the cost "don't-care" band, so it's monotone — once on the merged plan the next recompute reproduces it and there's nothing to switch. Genuine forecast jitter (fresh plan differs but is equally fragmented) leaves frag_new == frag_prev, so the previous plan is kept and the anti-jitter hysteresis is fully preserved.

Testing

  • New tests/test_plan_tiebreak.py (registered as plan_tiebreak) — 13 assertions covering the fragmentation count (gaps, mode changes, freeze/off inactive, depth-agnostic) and every decision branch, including cleaner-but-costlier → rejected. Written test-first.
  • End-to-end on the reported scenario: calculate_plan now logs "cost-neutral improvement but less fragmented (11 vs 12 segments), using new plan" and replaces the staircase with the merged block.
  • No regressions: quick suite, debug_cases, and optimise_windows/optimise_windows_kernel all pass; pre-commit (ruff/black/cspell) clean.

🤖 Generated with Claude Code

… plan

metric_min_improvement_plan keeps the incumbent plan whenever a freshly
optimised plan is not better by the threshold. Under flat export rates a
fragmented (split) export staircase and a clean single block are near-cost
equal, so once a split forms it is locked in forever even though every
recompute produces the cleaner block.

Add plan_fragmentation() (count of contiguous active charge/export segments,
split by idle gaps or charge<->export mode changes) and should_replace_plan().
On a near-tie the new plan is adopted only when it is no worse on cost
(improvement >= 0) and strictly less fragmented, so a cleaner block can replace
a locked-in staircase without churning the plan for tiny cost changes. The
existing "clearly better by the threshold" path and the anti-jitter hysteresis
for equally-fragmented plans are unchanged.

Add unit tests for the fragmentation count and every decision branch.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 24, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a near-tie tie-break in the planning pipeline to prevent “locked-in” fragmented export schedules under flat export rates by preferring the less-fragmented plan when the freshly optimised plan is not meaningfully better but is no worse on the optimisation metric.

Changes:

  • Added Plan.plan_fragmentation(...) and Plan.should_replace_plan(...) helpers to quantify plan fragmentation and decide near-tie plan replacement.
  • Wired the tie-break into Plan.calculate_plan() when comparing the incumbent vs recomputed plan.
  • Added and registered a focused unit test module covering fragmentation counting and tie-break decision branches.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
apps/predbat/plan.py Adds fragmentation scoring + near-tie replacement logic and integrates it into the plan comparison branch.
apps/predbat/tests/test_plan_tiebreak.py Adds regression/unit tests for fragmentation counting and plan replacement decisions.
apps/predbat/unit_test.py Registers the new plan_tiebreak test in the test runner.

Comment thread apps/predbat/plan.py
elif (metric_prev - metric) >= self.metric_min_improvement_plan:
self.log("New plan metric is significantly better from previous plan, using new plan")
else:
self.log("New plan is a cost-neutral improvement but less fragmented ({} vs {} segments), using new plan".format(fragmentation_new, fragmentation_prev))
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.

2 participants