Skip to content

Commit ea247f5

Browse files
committed
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
1 parent b75f4ad commit ea247f5

9 files changed

Lines changed: 102 additions & 96 deletions

File tree

docs/CHANGELOG.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,24 @@ The `scenario-scope` section of `optim-config.yml` now supports a full
1010
playlist mechanism. The old `nb-scenarios` integer key is removed and raises
1111
a validation error if still present.
1212

13+
Scenario indices are **0-based** throughout, consistent with the
14+
`modeler-scenariobuilder.dat` convention.
15+
1316
**Inline form** — specify scenarios with integers, string-integers, and
1417
inclusive `"a-b"` range strings:
1518

1619
~~~ yaml
1720
scenario-scope:
1821
include:
19-
- "1-50"
20-
- 75
21-
- "90-100"
22+
- "0-49"
23+
- 74
24+
- "89-99"
2225
exclude:
23-
- 10
24-
- 15
26+
- 9
27+
- 14
2528
~~~
2629

27-
**Playlist-file form** — point to a flat JSON array of 1-based integers,
30+
**Playlist-file form** — point to a flat JSON array of 0-based integers,
2831
useful for machine-generated playlists:
2932

3033
~~~ yaml

docs/user-guide/optim-config.md

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

29-
# Monte-Carlo scenarios to simulate (1-based, inline form)
29+
# Monte-Carlo scenarios to simulate (0-based, inline form)
3030
scenario-scope:
3131
include:
32-
- "1-10" # scenarios 1 through 10
32+
- "0-9" # scenarios 0 through 9 (10 scenarios)
3333

3434
# Solver settings
3535
solver-options:
@@ -67,9 +67,8 @@ The total number of timesteps solved is `last-time-step − first-time-step + 1`
6767

6868
## `scenario-scope`
6969

70-
Selects which Monte-Carlo scenarios to simulate. Indices in the YAML are
71-
**1-based** (matching the scenario-builder convention); GemsPy converts them
72-
to 0-based indices internally.
70+
Selects which Monte-Carlo scenarios to simulate. Indices are **0-based**,
71+
consistent with the `modeler-scenariobuilder.dat` file convention.
7372

7473
Two mutually exclusive forms are supported:
7574

@@ -87,41 +86,41 @@ Each entry in `include` or `exclude` may be:
8786

8887
- An integer: `5` → scenario 5
8988
- A string integer: `"5"` → scenario 5 (identical to `5`)
90-
- A range: `"1-10"` → scenarios 1 through 10 (inclusive)
89+
- A range: `"0-9"` → scenarios 0 through 9 inclusive (10 scenarios)
9190

9291
**Examples:**
9392

9493
~~~ yaml
9594
# Run a single scenario
9695
scenario-scope:
9796
include:
98-
- 1
97+
- 0
9998

100-
# Run scenarios 1 to 100
99+
# Run scenarios 0 to 99
101100
scenario-scope:
102101
include:
103-
- "1-100"
102+
- "0-99"
104103

105-
# Run scenarios 1–20 and 50–60, but skip 10 and 15
104+
# Run scenarios 0–19 and 49–59, but skip 9 and 14
106105
scenario-scope:
107106
include:
108-
- "1-20"
109-
- "50-60"
107+
- "0-19"
108+
- "49-59"
110109
exclude:
111-
- 10
112-
- 15
110+
- 9
111+
- 14
113112
~~~
114113

115114
**Rules:**
116115

117-
- All indices must be ≥ 1.
116+
- All indices must be ≥ 0.
118117
- Overlapping entries in `include` are deduplicated automatically.
119118
- Excludes that do not appear in `include` produce a warning and have no effect.
120119
- Output is always sorted in ascending order.
121120
- `exclude` cannot be used without `include`.
122121

123122
**Default behaviour** (no `scenario-scope` key at all, or an empty block):
124-
runs scenario 1 only.
123+
runs scenario 0 only.
125124

126125
---
127126

@@ -153,7 +152,7 @@ called, so any I/O or format errors surface immediately at load time.
153152
**Rules:**
154153

155154
- The file must be a flat JSON array of integers (no booleans, strings, or objects).
156-
- All indices must be ≥ 1.
155+
- All indices must be ≥ 0.
157156
- Duplicates are silently removed; the result is sorted ascending.
158157
- `include` / `exclude` and `playlist-file` are mutually exclusive.
159158

@@ -330,10 +329,10 @@ from gems.optim_config import (
330329
# Load from file (returns None if the file does not exist)
331330
config = load_optim_config(Path("my_study/input/optim-config.yml"))
332331

333-
# Build programmatically — inline form (scenarios 1–10)
332+
# Build programmatically — inline form (scenarios 0–9)
334333
config = OptimConfig(
335334
time_scope=TimeScopeConfig(first_time_step=0, last_time_step=8759),
336-
scenario_scope=ScenarioScopeConfig(include=["1-10"]),
335+
scenario_scope=ScenarioScopeConfig(include=["0-9"]),
337336
solver_options=SolverOptionsConfig(name="highs", logs=False),
338337
resolution=ResolutionConfig(
339338
mode=ResolutionMode.SEQUENTIAL_SUBPROBLEMS,

src/gems/optim_config/parsing.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -136,37 +136,29 @@ def parsed_parameters(self) -> Dict[str, Any]:
136136

137137

138138
def _expand_entries(entries: List[Union[int, str]]) -> Set[int]:
139-
"""Expand 1-based ints and inclusive 'a-b' range strings into a set of 0-based indices."""
139+
"""Expand 0-based ints and inclusive 'a-b' range strings into a set of 0-based indices."""
140140
result: Set[int] = set()
141141
for entry in entries:
142142
if isinstance(entry, int):
143-
if entry < 1:
144-
raise ValueError(f"Scenario index must be >= 1 (1-based), got {entry}")
145-
result.add(entry - 1)
143+
if entry < 0:
144+
raise ValueError(f"Scenario index must be >= 0, got {entry}")
145+
result.add(entry)
146146
else:
147147
s = str(entry).strip()
148148
if re.fullmatch(r"\d+", s):
149149
val = int(s)
150-
if val < 1:
151-
raise ValueError(
152-
f"Scenario index must be >= 1 (1-based), got {val}"
153-
)
154-
result.add(val - 1)
150+
result.add(val)
155151
else:
156152
match = re.fullmatch(r"(\d+)-(\d+)", s)
157153
if not match:
158154
raise ValueError(
159155
f"Invalid entry {entry!r}: expected an integer or a range 'a-b'"
160-
" (e.g. '5' or '1-10')"
156+
" (e.g. '5' or '0-9')"
161157
)
162158
a, b = int(match.group(1)), int(match.group(2))
163-
if a < 1:
164-
raise ValueError(
165-
f"Range start must be >= 1 (1-based), got {a} in {entry!r}"
166-
)
167159
if a > b:
168160
raise ValueError(f"Range start must be <= end, got {entry!r}")
169-
result.update(range(a - 1, b))
161+
result.update(range(a, b + 1))
170162
return result
171163

172164

@@ -227,19 +219,19 @@ def _load_playlist(self) -> List[int]:
227219
raise ValueError(
228220
f"'{self.playlist_file}' must contain a flat JSON array of integers"
229221
)
230-
if any(x < 1 for x in data):
222+
if any(x < 0 for x in data):
231223
raise ValueError(
232-
f"'{self.playlist_file}': all scenario indices must be >= 1 (1-based)"
224+
f"'{self.playlist_file}': all scenario indices must be >= 0"
233225
)
234-
return sorted({x - 1 for x in data})
226+
return sorted(set(data))
235227

236228
def _resolve_inline(self) -> List[int]:
237229
included = _expand_entries(self.include or [])
238230
excluded = _expand_entries(self.exclude or [])
239231
orphans = excluded - included
240232
if orphans:
241233
warnings.warn(
242-
f"Excluded scenario indices {sorted(o + 1 for o in orphans)} "
234+
f"Excluded scenario indices {sorted(orphans)} "
243235
"are not in the include set and have no effect",
244236
UserWarning,
245237
stacklevel=2,

tests/e2e/functional/studies/13_1/input/optim-config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ solver-options:
2020

2121
scenario-scope:
2222
include:
23-
- 1
23+
- 0
2424

2525
models:
2626
- id: lib_example_13_1.generator_with_continuous_invest

tests/e2e/functional/studies/13_2/input/optim-config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ solver-options:
2020

2121
scenario-scope:
2222
include:
23-
- 1
23+
- 0
2424

2525
models:
2626
- id: lib_example_13_2.generator_with_discrete_invest

tests/e2e/functional/studies/7_4/input/optim-config.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,4 @@ solver-options:
99

1010
scenario-scope:
1111
include:
12-
- 1
12+
- 0

tests/e2e/functional/test_optim_modes.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
parameters: ""
4343
scenario-scope:
4444
include:
45-
- 1
45+
- 0
4646
resolution:
4747
mode: frontal
4848
""")
@@ -57,7 +57,7 @@
5757
parameters: ""
5858
scenario-scope:
5959
include:
60-
- 1
60+
- 0
6161
resolution:
6262
mode: parallel-subproblems
6363
block-length: 168
@@ -73,7 +73,7 @@
7373
parameters: ""
7474
scenario-scope:
7575
include:
76-
- 1
76+
- 0
7777
resolution:
7878
mode: sequential-subproblems
7979
block-length: 168

tests/e2e/functional/test_rolling_horizon_suboptimality.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
parameters: ""
7979
scenario-scope:
8080
include:
81-
- 1
81+
- 0
8282
models:
8383
- id: rolling-horizon-lib.storage
8484
out-of-bounds-processing:

0 commit comments

Comments
 (0)