[GP-02] Sequential mode: configurable carry-over-length, correct stitching for block-overlap != 1 - #274
[GP-02] Sequential mode: configurable carry-over-length, correct stitching for block-overlap != 1#274aoustry wants to merge 16 commits into
Conversation
Add 'carry-over-length' to ResolutionConfig (issue #271): optional, defaults to block-overlap via effective_carry_over_length, validated as 0 <= carry-over-length <= block-overlap with no special case at block-overlap == 0. Also add the previously missing range check 0 <= block-overlap < block-length. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BsEZdPwCrguejkXxz33FS
…ial mode The sequential carry-over always extracted block N's last timestep and pinned it to block N+1's first timestep, which only refers to the same absolute timestep when block-overlap == 1; for overlap >= 2 it silently stitched the wrong pair of timesteps (issue #271). _extract_carry_over now extracts effective_carry_over_length values starting at local index block_length - block_overlap (the earliest shared timestep), re-indexed to time 0..k-1, and Phase 5 pins the next block's first k timesteps against them. Scalar initial_values (no time dim) keep the legacy single-timestep pin for direct build_problem callers. Behavioural consequence: with block-overlap: 0 nothing is carried between blocks any more (the previous implicit single-timestep seeding is gone) — state continuity now requires block-overlap >= 1. The new e2e test fails against the previous runtime behaviour for all four of its cases and passes with this fix; existing block-overlap: 1 e2e tests pass unmodified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BsEZdPwCrguejkXxz33FS
Document the new 'carry-over-length' resolution setting: parameter table entry, expanded sequential-subproblems section with an annotated timeline diagram (block-length / block-overlap / carry-over-length), default and explicit-zero semantics, validation rules, and a breaking-change warning for block-overlap: 0 configs. Add the corresponding CHANGELOG entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BsEZdPwCrguejkXxz33FS
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BsEZdPwCrguejkXxz33FS
With block-overlap: 0 no carry-over constraint is created: each block's initial state is free (block 1 serves its t=7 peak by pre-charging from a free SoC, which the old implicit single-timestep seeding made impossible), and the sequential result is identical to parallel-subproblems mode, which solves the same windows independently by construction. The test fails against the pre-#271 runtime behaviour. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018BsEZdPwCrguejkXxz33FS
| else: | ||
| # Scalar (no time dim): legacy form, pin the first timestep. | ||
| self.linopy_model.add_constraints( | ||
| linopy_var.isel(time=0) == init_val, # type: ignore[arg-type] | ||
| name=f"carry_over__{safe}", | ||
| ) |
There was a problem hiding this comment.
This scalar else branch looks unreachable: the only producer of initial_values, SimulationSession._extract_carry_over (session.py:263-272), always returns a DataArray with a "time" dim. Is there a live caller that still needs this legacy path, or can it be removed?
There was a problem hiding this comment.
Good point; I have to investigate more how the time-independent variables would have to be handled by the carry-over mechanism.
There was a problem hiding this comment.
Design choice: I propose not to fix any time-independant variables.
| """carry-over-length < block-overlap pins only the leading shared | ||
| timesteps; the rest of the overlap zone is re-optimized freely.""" | ||
| raw = _run(tmp_path, "partial_pin", _sequential_config("carry-over-length: 1")) | ||
| _assert_pinned_window_consistent(raw, carry_over_length=1) |
There was a problem hiding this comment.
I think we should also assert that the 2nd and 3rd overlapping timesteps may differ between the blocks N and N+1 (I hope the test already leads to different solutions when pinning or not these timesteps)
There was a problem hiding this comment.
Or even better, I think all tests in this test suite should rather focus on checking whether the bounds of the carried over variables are fixed / let free depending on the test case (or more precisely if an equality constraint exists for carried over variables). This is what we really want to test
| timesteps = set(raw["absolute_time_index"].dropna().astype(int)) | ||
| assert timesteps == set(range(12)) |
There was a problem hiding this comment.
I do not understand what is tested here, I have the feeling this test would pass whatever the carry over length (as all timesteps are anyway present in the simulation table). Like the previous test we may assert that at least the value of the first overlapping timestep differs accross successive block (or find at least one block where it is the case if the previous statement is too strong)
| Two complementary checks: | ||
|
|
||
| - Block 1 ([6..11], demand [2,4,0,4,4,0]) serves its t=7 peak by | ||
| pre-charging its *free* initial storage state. Under the old implicit |
There was a problem hiding this comment.
Remove reference to historical design
|
Functional question : Now for the use case with a storage on which we want continuity in the SoC, and let's say with weekly blocks, we should have |
| block_overlap: int = 0 | ||
| carry_over_length: Optional[int] = None |
There was a problem hiding this comment.
Should we raise an error (or a warning) when block_overlap and carry_over_length are set in a different mode than sequential to avoid silent ignore of these parameters
There was a problem hiding this comment.
Good catch - done in c97b584, as a hard error rather than a warning.
A new ResolutionConfig validator rejects block-overlap / carry-over-length in all three non-sequential modes (frontal, parallel-subproblems, benders-decomposition). parallel-subproblems was the motivating case: it does window the horizon, so an overlap there looks like it should do something and quietly does nothing.
| local_start=block_length - block_overlap, | ||
| length=carry_over_length, | ||
| ) | ||
| t_start += block_length - block_overlap |
There was a problem hiding this comment.
block_length - block_overlap computed twice, could be extracted to improve readability
Co-authored-by: tbittar <thomas.bittar@rte-france.com>
Refactor carry_over extraction to use updated t_start.
Updated the test description to remove issue reference.
…quential mode `block-overlap` and `carry-over-length` steer the stitching of consecutive blocks, which only exists in `sequential-subproblems`. They were silently dropped in the three other modes — most confusingly in `parallel-subproblems`, which does window the horizon but ignores the overlap — while the user guide already documented them as sequential-only. A new `ResolutionConfig` validator now rejects either key when the mode is not sequential. The check is on the keys the user actually wrote (`model_fields_set`), so an explicit `block-overlap: 0` is caught too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014b5oL6qL5BVN6MWwBLcJxc
…l_values contract (#282) * refactor(runner): drop the dead scalar carry-over branch, enforce the contract
…#283) * test(carry-over): assert the pinned constraint window, not the values Replace the four value-comparison tests of `carry-over-length` with one parametrized test that checks what the setting actually controls: which variables are fixed by a carry-over equality constraint and which are left free.
Process ID
Process: GP-02
Description
Closes #271.
In
sequential-subproblemsmode, the carry-over that stitches block N to block N+1 was hardcoded: it extracted block N's last timestep and pinned it to block N+1's first timestep. That pairing only refers to the same absolute timestep whenblock-overlap == 1; forblock-overlap >= 2it silently pinned the wrong pair of timesteps.This PR makes the carry-over an explicit
resolutionsetting and fixes the absolute-timestep alignment:carry-over-lengthadded toResolutionConfig(optional; omitted → defaults toblock-overlap, i.e. full pin of the overlap zone). Explicit0is legal and distinct from unset: blocks overlap for lag-constraint history but are not stitched at all.0 <= carry-over-length <= block-overlap(no special case atblock-overlap == 0), plus the previously missing range check0 <= block-overlap < block-length._extract_carry_overnow extractseffective_carry_over_lengthvalues starting at the earliest shared timestep (local indexblock_length - block_overlap) instead of always the last one; Phase 5 pins the next block's first k timesteps against them (clamped for truncated final blocks). Scalarinitial_valueskeep the legacy single-timestep pin for directbuild_problemcallers.docs/user-guide/optim-config.mddocuments the three parameters with an annotated timeline diagram (from the issue discussion), defaults, validation, and a breaking-change warning;docs/CHANGELOG.mdgains a BREAKING entry with the migration note.Breaking change: with
block-overlap: 0(the default) nothing is carried between blocks any more — the previous implicit single-timestep seeding is gone. Configs relying on state continuity (e.g. storage state-of-charge) must setblock-overlap: 1(and optionallycarry-over-length: 1).Impact Analysis
Affected modules:
optim_config/—ResolutionConfigschema: new field + validators (src/gems_craft/optim_config/parsing.py).gems_runner/session/—_run_sequential/_extract_carry_overgeneralized to a multi-timestep window.gems_runner/simulation/—optimization.pyPhase 5 carry-over constraints generalized;build_problemdocstring updated.docs/— user guide + changelog.Solver output values expected to change: yes, for sequential mode only:
block-overlap >= 2: outputs change because the previous stitching was incorrect (wrong absolute timestep pinned); the new behaviour pins each shared timestep to its matching absolute timestep.block-overlap: 0: outputs change because blocks no longer inherit the previous block's final state (breaking change above).block-overlap: 1: no change — the new default is identical to the old hardcoded single-timestep pin; the existing e2e tests (test_rolling_horizon_suboptimality.py,test_optim_modes.py) pass unmodified.frontal/parallel-subproblems/benders-decomposition: unaffected.Test coverage: 13 new unit tests for the validators and default resolution; new e2e module
test_sequential_carry_over_length.py(block-length 6, block-overlap 3, carry-over 3/1/0 over an aperiodic 12-step demand) asserting every pinned shared timestep is identical in both blocks — all four cases fail against the previous runtime behaviour and pass with the fix.Checklist
pytest) — 604 passed, 6 skipped, 2 xfailedmypy)black,isort)pyproject.tomlversion bumped if applicable — not applicable (version bumps are handled via thefeat(release):workflow)AGENTS.mdreviewed for impact and updated if needed — no update needed (resolution-mode internals are not described there)