Skip to content

Commit f9a9840

Browse files
Merge pull request #137 from switchbox-data/134-mvp-function-to-generate-electrical-tariff-map
Add MVP function for electrical tariff mapping
2 parents 006980d + 7e8a18b commit f9a9840

8 files changed

Lines changed: 615 additions & 327 deletions

File tree

pyproject.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ license = { text = "MIT" }
99
dependencies = [
1010
"buildstock-fetch>=1.6.1",
1111
"cairo",
12-
"polars",
12+
"cloudpathlib[s3]>=0.23.0",
1313
"numpy",
14+
"polars",
15+
"prek>=0.3.2",
1416
"pyarrow",
1517
]
1618

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Resolve project root via git (works from any subdirectory in the repo)
2+
project_root := `git rev-parse --show-toplevel`
3+
path_s3_resstock_metadata := "s3://data.sb/nrel/resstock/res_2024_amy2018_2/metadata"
4+
5+
# Usage: just map-electric-tariff [electric_utility] [SB_scenario_type] [SB_scenario_year] [upgrade_id]
6+
# Example: just map-electric-tariff Coned default 1 00
7+
map-electric-tariff electric_utility SB_scenario_type SB_scenario_year upgrade_id output_dir="":
8+
uv run python {{project_root}}/utils/electric_tariff_mapper.py \
9+
--metadata_path "{{path_s3_resstock_metadata}}" \
10+
--state NY \
11+
--upgrade_id "{{upgrade_id}}" \
12+
--electric_utility "{{electric_utility}}" \
13+
--SB_scenario_type "{{SB_scenario_type}}" \
14+
--SB_scenario_year "{{SB_scenario_year}}" \
15+
{{ if output_dir != "" { "--output_dir \"" + output_dir + "\"" } else { "" } }}

rate_design/ny/hp_rates/run_scenario.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
"""Entrypoint for running NY heat pump rate scenarios (stub)."""
22

33
import logging
4-
import pandas as pd
54
from pathlib import Path
5+
6+
import pandas as pd
67
from cairo.rates_tool.loads import _return_load, return_buildingstock
78
from cairo.rates_tool.systemsimulator import (
89
MeetRevenueSufficiencySystemWide,
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Resolve project root via git (works from any subdirectory in the repo)
2+
project_root := `git rev-parse --show-toplevel`
3+
path_s3_resstock_metadata := "s3://data.sb/nrel/resstock/res_2024_amy2018_2/metadata"
4+
5+
# Usage: just map-electric-tariff [electric_utility] [SB_scenario_type] [SB_scenario_year] [upgrade_id]
6+
# Example: just map-electric-tariff Coned default 1 00
7+
map-electric-tariff electric_utility SB_scenario_type SB_scenario_year upgrade_id output_dir="":
8+
uv run python {{project_root}}/utils/electric_tariff_mapper.py \
9+
--metadata_path "{{path_s3_resstock_metadata}}" \
10+
--state RI \
11+
--upgrade_id "{{upgrade_id}}" \
12+
--electric_utility "{{electric_utility}}" \
13+
--SB_scenario_type "{{SB_scenario_type}}" \
14+
--SB_scenario_year "{{SB_scenario_year}}"
15+
{{ if output_dir != "" { "--output_dir \"" + output_dir + "\"" } else { "" } }}

typos.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[default]
2+
3+
[default.extend-words]
4+
TOU = "TOU"

utils/electric_tariff_mapper.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import argparse
2+
from pathlib import Path
3+
from typing import cast
4+
5+
import polars as pl
6+
from cloudpathlib import S3Path
7+
8+
from utils.types import SBScenario, electric_utility
9+
10+
# Project root (rate-design-platform); independent of cwd or caller
11+
_PROJECT_ROOT = Path(__file__).resolve().parent.parent
12+
RATE_DESIGN_DIR = _PROJECT_ROOT / "rate_design"
13+
14+
AWS_REGION = "us-west-2"
15+
16+
STORAGE_OPTIONS = {"aws_region": AWS_REGION}
17+
18+
19+
def define_electrical_tariff_key(
20+
SB_scenario: SBScenario,
21+
electric_utility: electric_utility,
22+
has_hp: pl.Expr,
23+
) -> pl.Expr:
24+
if SB_scenario.analysis_type == "default":
25+
return pl.lit(f"{electric_utility}_{SB_scenario}_default")
26+
elif SB_scenario.analysis_type in ["seasonal", "class_specific_seasonal"]:
27+
return (
28+
pl.when(has_hp)
29+
.then(pl.lit(f"{electric_utility}_{SB_scenario}_HP.csv"))
30+
.otherwise(pl.lit(f"{electric_utility}_{SB_scenario}_flat.csv"))
31+
)
32+
else:
33+
raise ValueError(f"Invalid SB scenario: {SB_scenario}")
34+
35+
36+
def generate_electrical_tariff_mapping(
37+
lazy_metadata_has_hp: pl.LazyFrame,
38+
SB_scenario: SBScenario,
39+
electric_utility: electric_utility,
40+
) -> pl.LazyFrame:
41+
electrical_tariff_mapping_df = lazy_metadata_has_hp.select(
42+
pl.col("bldg_id"),
43+
define_electrical_tariff_key(
44+
SB_scenario, electric_utility, pl.col("postprocess_group.has_hp")
45+
).alias("tariff_key"),
46+
)
47+
48+
return electrical_tariff_mapping_df
49+
50+
51+
def map_electric_tariff(
52+
SB_metadata_lazy_df: pl.LazyFrame,
53+
electric_utility: electric_utility,
54+
SB_scenario: SBScenario,
55+
state: str,
56+
) -> pl.LazyFrame:
57+
utility_metadata_df = SB_metadata_lazy_df.filter(
58+
pl.col("sb.electric_utility") == electric_utility
59+
)
60+
61+
metadata_has_hp = utility_metadata_df.select(
62+
pl.col("bldg_id", "postprocess_group.has_hp")
63+
)
64+
65+
# Check if there are any rows in the filtered dataframe
66+
test_sample = cast(pl.DataFrame, metadata_has_hp.head(1).collect())
67+
if test_sample.is_empty():
68+
raise ValueError(f"No rows found for electric utility {electric_utility}")
69+
70+
electrical_tariff_mapping_df = generate_electrical_tariff_mapping(
71+
metadata_has_hp, SB_scenario, electric_utility
72+
)
73+
74+
return electrical_tariff_mapping_df
75+
76+
77+
if __name__ == "__main__":
78+
parser = argparse.ArgumentParser(description="Map electrical tariff.")
79+
parser.add_argument(
80+
"--metadata_path",
81+
required=True,
82+
help="Absolute or s3 path to ResStock metadata",
83+
)
84+
parser.add_argument("--state", required=True, help="State code (e.g. RI)")
85+
parser.add_argument("--upgrade_id", required=True, help="Upgrade id (e.g. 00)")
86+
parser.add_argument(
87+
"--electric_utility", required=True, help="Electric utility (e.g. Coned)"
88+
)
89+
parser.add_argument(
90+
"--SB_scenario_type",
91+
required=True,
92+
help="SB scenario type (e.g. default, seasonal, class_specific_seasonal)",
93+
)
94+
parser.add_argument(
95+
"--SB_scenario_year", required=True, help="SB scenario year (e.g. 2024)"
96+
)
97+
parser.add_argument(
98+
"--output_dir",
99+
default=None,
100+
help="Optional directory for output CSV; default is rate_design/<state>/hp_rates/data/tariff_map/",
101+
)
102+
args = parser.parse_args()
103+
104+
#########################################################
105+
# For now, we will manually add the electric utility name column. Later on, the metadata parquet will be updated to include this column.
106+
# Assign first ~1/3 to Coned, next ~1/3 to National Grid, last ~1/3 to NYSEG.
107+
try: # If the metadata path is an S3 path, use the S3Path class.
108+
base_path = S3Path(args.metadata_path)
109+
metadata_path = (
110+
base_path
111+
/ f"state={args.state}"
112+
/ f"upgrade={args.upgrade_id}"
113+
/ "metadata-sb.parquet"
114+
)
115+
if not metadata_path.exists():
116+
raise FileNotFoundError(f"Metadata path {metadata_path} does not exist")
117+
# Polars scan_parquet needs a string path; S3Path.as_uri() gives s3:// URL
118+
SB_metadata_lazy_df = pl.scan_parquet(
119+
str(metadata_path), storage_options=STORAGE_OPTIONS
120+
)
121+
except ValueError:
122+
# If the metadata path is a local path, use the Path class.
123+
base_path = Path(args.metadata_path)
124+
metadata_path = (
125+
base_path
126+
/ f"state={args.state}"
127+
/ f"upgrade={args.upgrade_id}"
128+
/ "metadata-sb.parquet"
129+
)
130+
if not metadata_path.exists():
131+
raise FileNotFoundError(f"Metadata path {metadata_path} does not exist")
132+
SB_metadata_lazy_df = pl.scan_parquet(str(metadata_path))
133+
134+
# Add dummy electric utility column (deterministic by bldg_id). Later this column will be pre-existing in the SB metadata parquet.
135+
SB_metadata_lazy_df_with_electric_utility = SB_metadata_lazy_df.with_columns(
136+
pl.when(pl.col("bldg_id").hash() % 3 == 0)
137+
.then(pl.lit("Coned"))
138+
.when(pl.col("bldg_id").hash() % 3 == 1)
139+
.then(pl.lit("National Grid"))
140+
.otherwise(pl.lit("NYSEG"))
141+
.alias("sb.electric_utility")
142+
)
143+
#########################################################
144+
145+
sb_scenario = SBScenario(args.SB_scenario_type, args.SB_scenario_year)
146+
electrical_tariff_mapping_df = map_electric_tariff(
147+
SB_metadata_lazy_df=SB_metadata_lazy_df_with_electric_utility,
148+
electric_utility=args.electric_utility,
149+
SB_scenario=sb_scenario,
150+
state=args.state,
151+
)
152+
if args.output_dir:
153+
try:
154+
base_path = S3Path(args.output_dir)
155+
output_path = base_path / f"{args.electric_utility}_{sb_scenario}.csv"
156+
if not output_path.parent.exists():
157+
output_path.parent.mkdir(parents=True)
158+
electrical_tariff_mapping_df.sink_csv(
159+
str(output_path), storage_options=STORAGE_OPTIONS
160+
)
161+
except ValueError:
162+
base_path = Path(args.output_dir)
163+
output_path = base_path / f"{args.electric_utility}_{sb_scenario}.csv"
164+
if not output_path.parent.exists():
165+
output_path.parent.mkdir(parents=True)
166+
electrical_tariff_mapping_df.sink_csv(str(output_path))
167+
else:
168+
output_path = (
169+
RATE_DESIGN_DIR
170+
/ args.state.lower()
171+
/ "hp_rates"
172+
/ "data"
173+
/ "tariff_map"
174+
/ f"{args.electric_utility}_{sb_scenario}.csv"
175+
)
176+
if not output_path.parent.exists():
177+
output_path.parent.mkdir(parents=True)
178+
electrical_tariff_mapping_df.sink_csv(str(output_path))

utils/types.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from typing import Literal
2+
3+
4+
class SBScenario:
5+
analysis_type: Literal["default", "seasonal", "class_specific_seasonal"]
6+
analysis_year: int
7+
8+
def __init__(
9+
self,
10+
analysis_type: Literal["default", "seasonal", "class_specific_seasonal"],
11+
analysis_year: int,
12+
):
13+
if analysis_type not in ["default", "seasonal", "class_specific_seasonal"]:
14+
raise ValueError(f"Invalid analysis type: {analysis_type}")
15+
self.analysis_type = analysis_type
16+
self.analysis_year = analysis_year
17+
18+
def __str__(self):
19+
return f"{self.analysis_type}_{self.analysis_year}"
20+
21+
22+
electric_utility = Literal["Coned", "National Grid", "NYSEG"]

0 commit comments

Comments
 (0)