Skip to content

[GP-02] Sequential mode: configurable carry-over-length, correct stitching for block-overlap != 1 - #274

Open
aoustry wants to merge 16 commits into
mainfrom
claude/issue-271-optimization-config-r9i6ns
Open

[GP-02] Sequential mode: configurable carry-over-length, correct stitching for block-overlap != 1#274
aoustry wants to merge 16 commits into
mainfrom
claude/issue-271-optimization-config-r9i6ns

Conversation

@aoustry

@aoustry aoustry commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Process ID

Process: GP-02

Description

Closes #271.

In sequential-subproblems mode, 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 when block-overlap == 1; for block-overlap >= 2 it silently pinned the wrong pair of timesteps.

This PR makes the carry-over an explicit resolution setting and fixes the absolute-timestep alignment:

  • carry-over-length added to ResolutionConfig (optional; omitted → defaults to block-overlap, i.e. full pin of the overlap zone). Explicit 0 is legal and distinct from unset: blocks overlap for lag-constraint history but are not stitched at all.
  • Validation: 0 <= carry-over-length <= block-overlap (no special case at block-overlap == 0), plus the previously missing range check 0 <= block-overlap < block-length.
  • Runtime: _extract_carry_over now extracts effective_carry_over_length values starting at the earliest shared timestep (local index block_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). Scalar initial_values keep the legacy single-timestep pin for direct build_problem callers.
  • Docs: docs/user-guide/optim-config.md documents the three parameters with an annotated timeline diagram (from the issue discussion), defaults, validation, and a breaking-change warning; docs/CHANGELOG.md gains 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 set block-overlap: 1 (and optionally carry-over-length: 1).

Impact Analysis

Affected modules:

  • optim_config/ResolutionConfig schema: new field + validators (src/gems_craft/optim_config/parsing.py).
  • gems_runner/session/_run_sequential / _extract_carry_over generalized to a multi-timestep window.
  • gems_runner/simulation/optimization.py Phase 5 carry-over constraints generalized; build_problem docstring 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

  • Unit tests pass (pytest) — 604 passed, 6 skipped, 2 xfailed
  • Type checking passes (mypy)
  • Formatting passes (black, isort)
  • pyproject.toml version bumped if applicable — not applicable (version bumps are handled via the feat(release): workflow)
  • AGENTS.md reviewed for impact and updated if needed — no update needed (resolution-mode internals are not described there)

claude added 3 commits August 14, 2026 15:15
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
Comment thread docs/user-guide/optim-config.md Outdated
aoustry and others added 5 commits August 14, 2026 17:19
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
Comment on lines +552 to +557
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}",
)

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.

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?

@aoustry aoustry Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Good point; I have to investigate more how the time-independent variables would have to be handled by the carry-over mechanism.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Design choice: I propose not to fix any time-independant variables.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Implementation in 9032635

Comment thread docs/user-guide/optim-config.md
Comment thread tests/e2e/functional/test_sequential_carry_over_length.py Outdated
"""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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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)

@tbittar tbittar Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in 7e5d0a8

Comment on lines +194 to +195
timesteps = set(raw["absolute_time_index"].dropna().astype(int))
assert timesteps == set(range(12))

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove reference to historical design

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

@tbittar

tbittar commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

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_length = 169 and block_overlap = carry_over_length = 1 ?

Comment on lines 155 to +156
block_overlap: int = 0
carry_over_length: Optional[int] = None

@tbittar tbittar Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

@aoustry aoustry Aug 19, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

Comment thread src/gems_runner/session/session.py Outdated
Comment on lines 120 to 123
local_start=block_length - block_overlap,
length=carry_over_length,
)
t_start += block_length - block_overlap

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

block_length - block_overlap computed twice, could be extracted to improve readability

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

done

aoustry and others added 6 commits August 19, 2026 11:54
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.
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.

[GP-02] Sequential mode: make carry-over-length a config, correct for block-overlap != 1

4 participants