Skip to content

Commit 82b23c0

Browse files
committed
Implement generate_adoption_scenario_yamls: per-year adoption scenario YAML
Reads a base scenario YAML and an adoption config, then emits a new scenarios_<utility>_adoption.yaml with one entry per (year x run) combination. For each generated entry: - path_resstock_metadata and path_resstock_loads are rewritten to point at the materialized data produced by materialize_mixed_upgrade. - year_run is updated to the calendar year for that adoption cohort. - All path strings containing year=<old> are updated to year=<new> so MC paths resolve to the correct Cambium year. - run_name gets a _y<year>_mixed tag inserted before the double- underscore tariff suffix. Run keys follow the scheme (year_index + 1) * 100 + run_num so keys are unique across (year, run) combinations and predictable when passed to run-adoption-scenario.
1 parent ea1e49a commit 82b23c0

1 file changed

Lines changed: 210 additions & 15 deletions

File tree

utils/pre/generate_adoption_scenario_yamls.py

Lines changed: 210 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,20 @@
44
YAML file (``scenarios_<utility>_adoption.yaml``) with one entry per
55
(year × run) combination. The per-year ``path_resstock_metadata`` and
66
``path_resstock_loads`` are rewritten to point at the materialized data
7-
produced by ``materialize_mixed_upgrade.py``. ``run_name`` is also extended
8-
with the year index and calendar year label.
7+
produced by ``materialize_mixed_upgrade.py``. ``year_run`` and all path
8+
strings containing ``year={old_year_run}`` are also updated to the calendar
9+
year for each generated entry.
10+
11+
Run keys in the output YAML use the scheme ``(year_index + 1) * 100 + run_num``:
12+
13+
- Year index 0 (first run year), base run 1 → key 101
14+
- Year index 0, base run 2 → key 102
15+
- Year index 1 (second run year), base run 1 → key 201
16+
- Year index 1, base run 2 → key 202
17+
- …
18+
19+
This ensures run keys are unique across (year, run) combinations and
20+
memorable when passed to ``run-adoption-scenario``.
921
1022
Usage
1123
-----
@@ -17,14 +29,19 @@
1729
--adoption-config rate_design/hp_rates/ny/config/adoption/nyca_electrification.yaml \\
1830
--materialized-dir /ebs/data/nrel/resstock/res_2024_amy2018_2_sb/adoption/nyca_electrification \\
1931
--output rate_design/hp_rates/ri/config/scenarios/scenarios_rie_adoption.yaml
20-
21-
TODO: implement body — this is a skeleton stub.
2232
"""
2333

2434
from __future__ import annotations
2535

2636
import argparse
37+
import copy
2738
import sys
39+
import warnings
40+
from pathlib import Path
41+
from typing import Any
42+
43+
import numpy as np
44+
import yaml
2845

2946

3047
def build_parser() -> argparse.ArgumentParser:
@@ -67,18 +84,196 @@ def build_parser() -> argparse.ArgumentParser:
6784
return p
6885

6986

87+
# ---------------------------------------------------------------------------
88+
# Adoption config helpers (mirrors materialize_mixed_upgrade logic)
89+
# ---------------------------------------------------------------------------
90+
91+
92+
def _load_yaml(path: Path) -> dict[str, Any]:
93+
with open(path, encoding="utf-8") as f:
94+
return yaml.safe_load(f)
95+
96+
97+
def _resolve_run_years(config: dict[str, Any]) -> list[tuple[int, int]]:
98+
"""Return ``[(year_index, calendar_year), ...]`` to generate entries for.
99+
100+
Uses ``run_years`` from the config when present; otherwise uses all
101+
``year_labels``. Snaps run_years entries to the nearest year_label when
102+
an exact match is not found.
103+
"""
104+
year_labels: list[int] = [int(y) for y in config["year_labels"]]
105+
run_years_raw: list[int] | None = config.get("run_years")
106+
107+
if run_years_raw is None:
108+
return list(enumerate(year_labels))
109+
110+
result: list[tuple[int, int]] = []
111+
for yr in run_years_raw:
112+
distances = [abs(yl - int(yr)) for yl in year_labels]
113+
nearest_idx = int(np.argmin(distances))
114+
nearest_year = year_labels[nearest_idx]
115+
if nearest_year != int(yr):
116+
warnings.warn(
117+
f"run_years entry {yr} not in year_labels; "
118+
f"snapping to {nearest_year} (index {nearest_idx})",
119+
stacklevel=2,
120+
)
121+
result.append((nearest_idx, nearest_year))
122+
return result
123+
124+
125+
# ---------------------------------------------------------------------------
126+
# Config transformation helpers
127+
# ---------------------------------------------------------------------------
128+
129+
130+
def _replace_year_in_value(value: Any, old_year: int, new_year: int) -> Any:
131+
"""Recursively replace ``year={old_year}`` with ``year={new_year}`` in strings."""
132+
if isinstance(value, str):
133+
return value.replace(f"year={old_year}", f"year={new_year}")
134+
if isinstance(value, dict):
135+
return {
136+
k: _replace_year_in_value(v, old_year, new_year) for k, v in value.items()
137+
}
138+
if isinstance(value, list):
139+
return [_replace_year_in_value(item, old_year, new_year) for item in value]
140+
return value
141+
142+
143+
def _insert_blank_lines_between_runs(yaml_str: str) -> str:
144+
"""Insert a blank line before run keys 2+, not before the first run key."""
145+
lines = yaml_str.splitlines()
146+
out: list[str] = []
147+
seen_run_key = False
148+
for line in lines:
149+
stripped = line.strip()
150+
is_run_key = (
151+
line.startswith(" ") and stripped.endswith(":") and stripped[:-1].isdigit()
152+
)
153+
if is_run_key and seen_run_key and (not out or out[-1] != ""):
154+
out.append("")
155+
if is_run_key:
156+
seen_run_key = True
157+
out.append(line)
158+
return "\n".join(out) + ("\n" if yaml_str.endswith("\n") else "")
159+
160+
161+
def _update_run_name(run_name: str, calendar_year: int) -> str:
162+
"""Append ``_y{year}_mixed`` to a run name (before any trailing double-underscore suffix).
163+
164+
Examples:
165+
``ri_rie_run1_up00_precalc__flat`` → ``ri_rie_run1_y2025_mixed_precalc__flat``
166+
``ny_nyseg_run5_up02_default__tou`` → ``ny_nyseg_run5_y2025_mixed_default__tou``
167+
"""
168+
# Locate the first double-underscore which separates the "stem" from the tariff suffix.
169+
double_us = run_name.find("__")
170+
year_tag = f"_y{calendar_year}_mixed"
171+
if double_us == -1:
172+
return run_name + year_tag
173+
return run_name[:double_us] + year_tag + run_name[double_us:]
174+
175+
176+
# ---------------------------------------------------------------------------
177+
# Main
178+
# ---------------------------------------------------------------------------
179+
180+
70181
def main(argv: list[str] | None = None) -> None:
71-
build_parser().parse_args(argv)
72-
# TODO: implement
73-
# 1. Load adoption config YAML for year_labels and scenario_name.
74-
# 2. Load base scenario YAML and extract the specified run configs.
75-
# 3. For each year index and each run number:
76-
# a. Copy the run config.
77-
# b. Replace path_resstock_metadata → <materialized_dir>/year=<N>/metadata-sb.parquet
78-
# c. Replace path_resstock_loads → <materialized_dir>/year=<N>/loads/
79-
# d. Update run_name to include year index and label.
80-
# 4. Write combined YAML to args.path_output.
81-
raise NotImplementedError("generate_adoption_scenario_yamls is not yet implemented")
182+
args = build_parser().parse_args(argv)
183+
184+
path_base_scenario = Path(args.path_base_scenario)
185+
path_adoption_config = Path(args.path_adoption_config)
186+
path_materialized_dir = Path(args.path_materialized_dir)
187+
path_output = Path(args.path_output)
188+
189+
# Parse run numbers.
190+
try:
191+
run_nums = [int(r.strip()) for r in args.runs.split(",") if r.strip()]
192+
except ValueError as exc:
193+
raise ValueError(
194+
f"--runs must be comma-separated integers, got: {args.runs!r}"
195+
) from exc
196+
if not run_nums:
197+
raise ValueError("--runs is empty; at least one run number is required.")
198+
199+
# 1. Load adoption config for year info.
200+
adoption_config = _load_yaml(path_adoption_config)
201+
scenario_name: str = adoption_config["scenario_name"]
202+
year_run_pairs = _resolve_run_years(adoption_config)
203+
204+
# 2. Load base scenario YAML and extract requested run configs.
205+
base_doc = _load_yaml(path_base_scenario)
206+
base_runs: dict[int, dict[str, Any]] = {
207+
int(k): v for k, v in base_doc.get("runs", {}).items()
208+
}
209+
210+
missing_runs = [r for r in run_nums if r not in base_runs]
211+
if missing_runs:
212+
available = sorted(base_runs.keys())
213+
raise KeyError(
214+
f"Run(s) {missing_runs} not found in {path_base_scenario}. "
215+
f"Available runs: {available}"
216+
)
217+
218+
print(
219+
f"Generating adoption scenario YAML for '{scenario_name}': "
220+
f"{len(year_run_pairs)} year(s) × {len(run_nums)} run(s) = "
221+
f"{len(year_run_pairs) * len(run_nums)} entries"
222+
)
223+
224+
# 3. Build generated run entries.
225+
output_runs: dict[int, dict[str, Any]] = {}
226+
227+
for year_index, calendar_year in year_run_pairs:
228+
meta_path = str(
229+
path_materialized_dir / f"year={calendar_year}" / "metadata-sb.parquet"
230+
)
231+
loads_path = str(path_materialized_dir / f"year={calendar_year}" / "loads" / "")
232+
233+
for run_num in run_nums:
234+
base_run = base_runs[run_num]
235+
old_year_run = int(base_run.get("year_run", calendar_year))
236+
237+
# Deep-copy so base configs remain unmodified.
238+
run_entry: dict[str, Any] = copy.deepcopy(base_run)
239+
240+
# Replace ResStock data paths.
241+
run_entry["path_resstock_metadata"] = meta_path
242+
run_entry["path_resstock_loads"] = loads_path
243+
244+
# Update year_run to the calendar year for this adoption cohort.
245+
run_entry["year_run"] = calendar_year
246+
247+
# Replace year= tokens in all string path values so MC data resolves
248+
# to the correct Cambium year.
249+
run_entry = _replace_year_in_value(run_entry, old_year_run, calendar_year)
250+
251+
# Update run_name to include year and mixed tag.
252+
run_entry["run_name"] = _update_run_name(
253+
str(base_run.get("run_name", f"run{run_num}")),
254+
calendar_year,
255+
)
256+
257+
output_key = (year_index + 1) * 100 + run_num
258+
output_runs[output_key] = run_entry
259+
print(
260+
f" [{output_key}] year={calendar_year}, "
261+
f"base_run={run_num}: {run_entry['run_name']}"
262+
)
263+
264+
# 4. Write combined YAML.
265+
path_output.parent.mkdir(parents=True, exist_ok=True)
266+
payload: dict[str, Any] = {"runs": output_runs}
267+
yaml_str = yaml.dump(
268+
payload,
269+
default_flow_style=False,
270+
sort_keys=False,
271+
allow_unicode=True,
272+
)
273+
yaml_str = _insert_blank_lines_between_runs(yaml_str)
274+
path_output.write_text(yaml_str, encoding="utf-8")
275+
276+
print(f"Wrote {len(output_runs)} run entries to {path_output}")
82277

83278

84279
if __name__ == "__main__":

0 commit comments

Comments
 (0)