Skip to content

Commit 77a743c

Browse files
aoustryclaude
andauthored
Replace nb-scenarios with scenario playlist in scenario-scope (#206)
* Replace nb-scenarios with scenario playlist in scenario-scope The scenario-scope config now accepts either an inline include/exclude declaration (with support for 1-based integer indices and 'a-b' range strings) or a playlist-file reference pointing to a flat JSON array of 1-based integers. Both forms resolve to a sorted, deduplicated list of 0-based MC scenario indices consumed by the solver. nb-scenarios is removed: the pydantic extra="forbid" policy makes any YAML that still uses it a hard validation error. All test fixtures and inline config strings have been migrated to the new include form. validate_optim_config gains an optional scenario_builder parameter to cross-check playlist indices against every defined scenario group. https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Update uv.lock after dependency resolution https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Fix black and isort formatting Two lines wrapped to 88 chars in parsing.py; one extra blank line removed after import in the test file. https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Improve scenario-scope: string ints, eager cache, missing tests - _expand_entries now accepts string integers like "5" in addition to int 5 and range "1-10", treating them identically after stripping - ScenarioScopeConfig stores scenario_ids in a PrivateAttr cache so resolution happens at most once per instance - load_optim_config triggers eager resolution after path resolution, so the playlist file is read exactly once at load time and I/O errors surface immediately - New tests: string-integer entries, cache identity (file deleted after load), and validate_optim_config ScenarioBuilder cross-check https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Fix playlist loading: wrap I/O errors as ValueError, reject JSON booleans FileNotFoundError and json.JSONDecodeError from _load_playlist now surface as ValueError so callers always receive a consistent exception type. JSON boolean values (true/false) are now explicitly rejected — bool is a subclass of int in Python so they previously slipped through the isinstance check silently. Three new tests cover each case. https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Update documentation for scenario-scope playlist feature - optim-config.md: replace nb-scenarios section with full description of the inline (include/exclude) and playlist-file forms, with examples, validation rules, and updated Python API snippets - scenario-builder.md: remove stale nb-scenarios reference; link to validate_optim_config cross-check - CHANGELOG.md: add [Unreleased] entry summarising the playlist changes; fix the 0.1.0 entry that still mentioned nb_scenarios https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Reject boolean values in inline include/exclude lists Pydantic coerces True/False to 1/0 before _expand_entries sees them, so the check must happen at the field_validator(mode="before") layer. A field_validator on include and exclude now raises ValueError for any bool item, making the behaviour consistent with playlist-file which already rejects JSON booleans. Two new tests cover include=[True] and exclude=[False]. https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Switch scenario-scope to 0-based indexing throughout All user-facing scenario index surfaces (optim-config.yml include/exclude and JSON playlist files) now use 0-based indices, consistent with the modeler-scenariobuilder.dat convention. The internal representation is unchanged (already 0-based), so only the boundary conversion code is affected: - _expand_entries: removed the '- 1' offset; lower bound is now >= 0 - _load_playlist: removed the '- 1' offset; lower bound is now >= 0 - _resolve_inline warning: no longer adds 1 back when reporting orphans - Range 'a-b' now means [a, b] inclusive (both 0-based) All test fixtures, inline config strings, unit tests and documentation updated accordingly. 'include: [0]' now means the first scenario (was 1). https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Fix CHANGELOG: restore accurate 0.1.0 entry, add inline boolean note - 0.1.0 entry: restore nb_scenarios description (that is what was shipped in 0.1.0; the playlist feature is in [Unreleased]) - [Unreleased] other-changes bullet: broaden boolean rejection note to cover both inline include/exclude lists and JSON playlist files https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Allow exclude with playlist-file; update docstrings throughout exclude is no longer restricted to the inline include form — it now applies to both base-set forms (include and playlist-file). The exclude logic was lifted out of _resolve_inline (now removed) into _compute_scenario_ids so it runs uniformly after the base set is built. - _check_constraints: 'exclude' now requires include OR playlist-file - _load_playlist: returns Set[int] instead of List[int]; sorting deferred to _compute_scenario_ids after exclude is applied - _compute_scenario_ids: single unified method handles both forms + exclude - Warning message updated: 'include set' → 'base set' Docstrings updated: - ScenarioScopeConfig: full class docstring documenting both forms, exclude compatibility, caching behaviour, and index convention - _expand_entries: clarified entry formats and boolean handling note - validate_optim_config: enumerated bullet-point list of all checks - ScenarioBuilder.validate_mc_scenarios: documents return contract Four new tests cover playlist-file + exclude (basic, range, all-excluded, orphan warning). Updated test for the relaxed exclude-without-base error. https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU * Fix validate_mc_scenarios error message; update load_optim_config docstring validate_mc_scenarios was still converting indices to 1-based in its error message after the 0-based switch. Error now reports the raw 0-based indices and the correct range (0–N-1). load_optim_config docstring replaced: removes the stale 'components_path' reference and documents the two extra steps the function performs beyond plain YAML parsing (relative path resolution and eager scenario_ids population). https://claude.ai/code/session_01EfjU9tBfdyZ9nZmQFCe5uU --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2178cfb commit 77a743c

15 files changed

Lines changed: 820 additions & 41 deletions

File tree

docs/CHANGELOG.md

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,54 @@
22

33
All notable changes to GemsPy are documented here.
44

5+
## [Unreleased]
6+
7+
### Scenario-scope playlist (replaces `nb-scenarios`)
8+
9+
The `scenario-scope` section of `optim-config.yml` now supports a full
10+
playlist mechanism. The old `nb-scenarios` integer key is removed and raises
11+
a validation error if still present.
12+
13+
Scenario indices are **0-based** throughout, consistent with the
14+
`modeler-scenariobuilder.dat` convention.
15+
16+
**Inline form** — specify scenarios with integers, string-integers, and
17+
inclusive `"a-b"` range strings:
18+
19+
~~~ yaml
20+
scenario-scope:
21+
include:
22+
- "0-49"
23+
- 74
24+
- "89-99"
25+
exclude:
26+
- 9
27+
- 14
28+
~~~
29+
30+
**Playlist-file form** — point to a flat JSON array of 0-based integers,
31+
useful for machine-generated playlists:
32+
33+
~~~ yaml
34+
scenario-scope:
35+
playlist-file: mc_playlist.json
36+
~~~
37+
38+
Other changes:
39+
40+
- `exclude` is now compatible with both `include` and `playlist-file`.
41+
Use it to subtract a few scenarios at run time without modifying the
42+
playlist file.
43+
- `validate_optim_config()` now accepts an optional `scenario_builder`
44+
argument and cross-checks all playlist indices against every scenario group,
45+
raising a `ValueError` for out-of-bounds indices.
46+
- The playlist is resolved and cached exactly once at `load_optim_config()`
47+
time; I/O and format errors surface immediately as `ValueError`.
48+
- Boolean values (`true`/`false`) are explicitly rejected in both inline
49+
lists and JSON playlist files.
50+
51+
---
52+
553
## [0.1.0] - 2026-04-30
654

755
### Study folder structure
@@ -26,7 +74,7 @@ A new `optim-config.yml` file controls all aspects of a simulation run:
2674
- **`resolution.mode`** — four strategies: `frontal`, `sequential-subproblems`, `parallel-subproblems`, `benders-decomposition`
2775
- **`resolution.block_length` / `block_overlap`** — time-window size and overlap for sequential/parallel modes
2876
- **`time_scope`** — `first_time_step` / `last_time_step`
29-
- **`scenario_scope.nb_scenarios`** — number of Monte-Carlo scenarios to run
77+
- **`scenario_scope.nb_scenarios`** — number of Monte-Carlo scenarios to run (replaced by the playlist mechanism in a later release)
3078
- **`solver_options`** — solver name (default: HiGHS), log verbosity, and free-form solver parameters
3179
- **`models[].model_decomposition`** — per-model assignment of variables, constraints, and objective contributions to `master`, `subproblems`, or `master-and-subproblems` (used for Benders decomposition)
3280
- **`models[].out_of_bounds_processing`** — per-constraint handling of time indices that fall outside the horizon (`cyclic` or `drop`)

docs/user-guide/optim-config.md

Lines changed: 118 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,10 @@ time-scope:
2626
first-time-step: 0
2727
last-time-step: 8759 # 8760 hourly timesteps → one year
2828

29-
# Number of Monte-Carlo scenarios to run
29+
# Monte-Carlo scenarios to simulate (0-based, inline form)
3030
scenario-scope:
31-
nb-scenarios: 10
31+
include:
32+
- "0-9" # scenarios 0 through 9 (10 scenarios)
3233

3334
# Solver settings
3435
solver-options:
@@ -66,13 +67,115 @@ The total number of timesteps solved is `last-time-step − first-time-step + 1`
6667

6768
## `scenario-scope`
6869

70+
Selects which Monte-Carlo scenarios to simulate. Indices are **0-based**,
71+
consistent with the `modeler-scenariobuilder.dat` file convention.
72+
73+
The base scenario set is defined by exactly one of two mutually exclusive
74+
keys: `include` (inline) or `playlist-file` (from a JSON file). `exclude`
75+
is optional and applies to **either** form.
76+
77+
### Inline form (`include` / `exclude`)
78+
79+
Specify scenarios directly in the YAML using individual integers, string
80+
integers, and inclusive `"a-b"` range strings.
81+
6982
| Key | Type | Default | Description |
7083
|---|---|---|---|
71-
| `nb-scenarios` | int | `1` | Number of Monte-Carlo scenarios |
84+
| `include` | list | — | Scenarios to run (required in inline form) |
85+
| `exclude` | list | — | Scenarios to remove from the base set (optional) |
86+
87+
Each entry in `include` or `exclude` may be:
88+
89+
- An integer: `5` → scenario 5
90+
- A string integer: `"5"` → scenario 5 (identical to `5`)
91+
- A range: `"0-9"` → scenarios 0 through 9 inclusive (10 scenarios)
7292

73-
Scenario indices run from `0` to `nb-scenarios − 1`. If a
74-
[scenario builder](scenario-builder.md) file is present, these indices are
75-
mapped to data-series columns by that file.
93+
**Examples:**
94+
95+
~~~ yaml
96+
# Run a single scenario
97+
scenario-scope:
98+
include:
99+
- 0
100+
101+
# Run scenarios 0 to 99
102+
scenario-scope:
103+
include:
104+
- "0-99"
105+
106+
# Run scenarios 0–19 and 49–59, but skip 9 and 14
107+
scenario-scope:
108+
include:
109+
- "0-19"
110+
- "49-59"
111+
exclude:
112+
- 9
113+
- 14
114+
~~~
115+
116+
**Rules:**
117+
118+
- All indices must be ≥ 0.
119+
- Overlapping entries in `include` are deduplicated automatically.
120+
- Excludes that do not appear in the base set produce a warning and have no effect.
121+
- Output is always sorted in ascending order.
122+
- `exclude` cannot be used without `include` or `playlist-file`.
123+
124+
**Default behaviour** (no `scenario-scope` key at all, or an empty block):
125+
runs scenario 0 only.
126+
127+
---
128+
129+
### Playlist-file form (`playlist-file`)
130+
131+
Point to a JSON file containing a flat array of 0-based integer scenario
132+
indices. Useful when the list of scenarios is generated programmatically or
133+
is too large to embed in YAML.
134+
135+
| Key | Type | Description |
136+
|---|---|---|
137+
| `playlist-file` | path | Path to a JSON playlist (relative to `optim-config.yml`) |
138+
139+
~~~ yaml
140+
scenario-scope:
141+
playlist-file: mc_playlist.json # resolved relative to optim-config.yml
142+
~~~
143+
144+
The referenced file must contain a flat JSON array of non-negative integers:
145+
146+
~~~ json
147+
[0, 2, 4, 6, 8, 10, 12]
148+
~~~
149+
150+
`exclude` can be combined with `playlist-file` to subtract specific scenarios
151+
at run time without modifying the file:
152+
153+
~~~ yaml
154+
scenario-scope:
155+
playlist-file: mc_playlist.json
156+
exclude:
157+
- 4
158+
- "8-10"
159+
~~~
160+
161+
GemsPy reads and validates the playlist eagerly when `load_optim_config()` is
162+
called, so any I/O or format errors surface immediately at load time.
163+
164+
**Rules:**
165+
166+
- The file must be a flat JSON array of integers (no booleans, strings, or objects).
167+
- All indices must be ≥ 0.
168+
- Duplicates are silently removed; the result is sorted ascending.
169+
- `include` and `playlist-file` are mutually exclusive.
170+
171+
---
172+
173+
### ScenarioBuilder cross-validation
174+
175+
If a [scenario builder](scenario-builder.md) file is present,
176+
`validate_optim_config()` checks that every scenario index in the playlist is
177+
defined for every scenario group. Out-of-bounds indices raise a `ValueError`
178+
listing the affected groups.
76179

77180
---
78181

@@ -238,17 +341,24 @@ from gems.optim_config import (
238341
# Load from file (returns None if the file does not exist)
239342
config = load_optim_config(Path("my_study/input/optim-config.yml"))
240343

241-
# Build programmatically
344+
# Build programmatically — inline form (scenarios 0–9)
242345
config = OptimConfig(
243346
time_scope=TimeScopeConfig(first_time_step=0, last_time_step=8759),
244-
scenario_scope=ScenarioScopeConfig(nb_scenarios=10),
347+
scenario_scope=ScenarioScopeConfig(include=["0-9"]),
245348
solver_options=SolverOptionsConfig(name="highs", logs=False),
246349
resolution=ResolutionConfig(
247350
mode=ResolutionMode.SEQUENTIAL_SUBPROBLEMS,
248351
block_length=168,
249352
),
250353
)
251354

355+
# Build programmatically — playlist-file form
356+
from pathlib import Path
357+
config_pf = OptimConfig(
358+
time_scope=TimeScopeConfig(first_time_step=0, last_time_step=8759),
359+
scenario_scope=ScenarioScopeConfig(playlist_file=Path("mc_playlist.json")),
360+
)
361+
252362
# Pass to SimulationSession
253363
from gems.session import SimulationSession
254364
from gems.study import load_study

docs/user-guide/scenario-builder.md

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ scenario index `i` always maps to column `i` of every timeseries file.
88

99
## Motivation
1010

11-
A study with `nb-scenarios = 100` may have some components (e.g. wind farms)
12-
whose data only has 10 distinct columns, while others (e.g. demand) have 100.
13-
The scenario builder lets you express this mapping explicitly, so GemsPy reads
14-
the right column for each component and scenario.
11+
A study that simulates 100 MC scenarios may have some components (e.g. wind
12+
farms) whose data only has 10 distinct columns, while others (e.g. demand)
13+
have 100. The scenario builder lets you express this mapping explicitly, so
14+
GemsPy reads the right column for each component and scenario.
1515

1616
---
1717

@@ -55,9 +55,11 @@ demand, 3 = 4
5555
```
5656

5757
!!! note
58-
All MC scenario indices (`0` to `nb-scenarios - 1`) must be listed for every
59-
group that appears in the file. Missing entries raise a `ValueError` at
60-
load time.
58+
Every MC scenario index that appears in the simulation playlist must be
59+
listed for every group in the file. Missing entries raise a `ValueError`
60+
at load time. `validate_optim_config()` cross-checks the
61+
[scenario-scope playlist](optim-config.md#scenario-scope) against the
62+
scenario builder and reports any out-of-bounds indices.
6163

6264
---
6365

0 commit comments

Comments
 (0)