Skip to content

Accurate thermal heuristic's cyclic min-up/down model is unsound in sequential-subproblems mode #268

Description

@aoustry

Summary

The accurate thermal heuristic (find_num_units_accurate, src/gems_runner/simulation/thermal_heuristic.py) enforces min-up/min-down duration on unit counts using a small LP whose time indices are cyclic ("All indices are cyclic (weekly wrap-around)", implemented via .roll() on num_starting/num_stopping/num_outages). That assumption is correct when the block being solved is the whole horizon (resolution.mode: frontal), but it is not adapted to resolution.mode: sequential-subproblems, where the horizon is split into independent blocks solved one after another with carry-over.

Where this breaks down

In SimulationSession._run_block (src/gems_runner/session/session.py), each block:

  1. builds a fresh linopy.Model() for just that block's timesteps,
  2. solves once (relaxed for integer-strategy: heuristic components),
  3. calls apply_thermal_heuristics, which invokes the heuristic per block, and
  4. re-solves.

_run_sequential (session.py:90-120) then calls _extract_carry_over, which pins var[time=0] == value for the next block using the previous block's last-timestep solution — a forward-only mechanism.

The accurate heuristic's cyclic wraparound instead ties the end of the current block back onto the start of the same block (via .roll(dim_0=shift) across the full block length in the min_up/min_down constraints). Consequences:

  • A unit whose min-up-time obligation starts near the end of block N should propagate its "must stay on" requirement forward into block N+1. Instead, the heuristic wraps that obligation onto the beginning of block N, which is already fixed by the carry-over constraint inherited from block N-1. The obligation is enforced in the wrong place, and it is silently lost at the block boundary.
  • Nothing in _extract_carry_over propagates a "still owes uptime/downtime" state between blocks — it only pins the raw variable value at time=0, not any remaining-duration counter — so even if the cyclic issue were fixed, min-up/down guarantees are not structurally carried across block boundaries.

By contrast, find_min_generation_fast does not have this issue: it treats block borders as a distinct max-of-border region (not a wraparound), so it degrades more gracefully across block seams.

Why this is currently unguarded

  • validate_optim_config (src/gems_craft/optim_config/validation.py) explicitly rejects integer-strategy: heuristic combined with resolution.mode: benders-decomposition (_check_no_heuristic_with_benders), but there is no equivalent check or warning for heuristic + sequential-subproblems.
  • The existing e2e coverage (tests/e2e/functional/test_thermal_heuristic_*.py, using tests/e2e/functional/optim-config/thermal_variants_for_heuristic.yml) only exercises a single 168-timestep block, i.e. frontal mode. There is no test combining thermal heuristics with sequential-subproblems, so this gap is currently unverified by CI.

Suggested next steps (for discussion, not prescriptive)

  • Add an e2e test running the accurate heuristic under sequential-subproblems with block-length smaller than the min-up/min-down durations, to make the boundary artifact reproducible/observable.
  • Either:
    • (a) make find_num_units_accurate block-boundary-aware (e.g. accept a real "units already committed near the end of the previous block" state instead of assuming cyclic wraparound), or
    • (b) explicitly document/validate that heuristic-id: accurate combined with sequential-subproblems produces only approximate min-up/down guarantees at block seams — similar to the existing Benders-decomposition guard, but as a warning rather than a hard error if (a) is judged out of scope.

Pointers

  • src/gems_runner/simulation/thermal_heuristic.py (find_num_units_accurate, cyclic .roll() constraints)
  • src/gems_runner/simulation/heuristic_runner.py (apply_thermal_heuristics, called per block)
  • src/gems_runner/session/session.py (_run_block, _run_sequential, _extract_carry_over)
  • src/gems_craft/optim_config/validation.py (_check_no_heuristic_with_benders — the existing precedent for a resolution-mode/heuristic compatibility guard)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions