From 6af0647c48ec5a969d6f73dfbe045c3c945fc46d Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Thu, 26 Mar 2026 16:45:26 -0400 Subject: [PATCH 01/40] Add EPMC residual allocation and make allocation method explicit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add equi-proportional marginal cost (EPMC) residual allocation to CAIRO via monkey-patches in utils/mid/patches.py. EPMC allocates residual costs in proportion to each customer's economic burden (R_i = R * EB_i / sum(EB_j * w_j)), equivalent to scaling all MC-based rates by K = TRR / MC_Revenue. - Disable broken peak residual allocation to free up compute. - Make build_master_bat.py detect available BAT metrics at runtime instead of hardcoding — gracefully handles missing metrics (logs and skips). - Refactor compute_subclass_rr to compute both per-customer and EPMC subclass RRs in a single pass. Revenue requirement YAML now uses nested format keyed by allocation method (percustomer, epmc). - Add required residual_allocation field to scenario YAML for subclass runs. Parser raises ValueError if field is missing or allocation method not found in the YAML. No defaults — every subclass run must be explicit. - Wire RI runs 17-18 to use EPMC allocation; runs 5-14 explicitly tagged as percustomer. NY runs 5-14 also tagged percustomer. - Migrate all *_hp_vs_nonhp.yaml files (RI + NY) to nested format. Made-with: Cursor --- rate_design/hp_rates/Justfile | 4 +- .../rev_requirement/cenhud_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/coned_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/nimo_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/nyseg_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/or_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/psegli_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/rge_hp_vs_nonhp.yaml | 20 +- .../ny/config/scenarios/scenarios_cenhud.yaml | 6 + .../ny/config/scenarios/scenarios_coned.yaml | 6 + .../ny/config/scenarios/scenarios_nimo.yaml | 6 + .../ny/config/scenarios/scenarios_nyseg.yaml | 6 + .../ny/config/scenarios/scenarios_or.yaml | 6 + .../ny/config/scenarios/scenarios_psegli.yaml | 6 + .../ny/config/scenarios/scenarios_rge.yaml | 6 + .../electric/cenhud_default_calibrated.json | 2 +- .../cenhud_default_supply_calibrated.json | 2 +- .../cenhud_nonhp_default_calibrated.json | 2 +- ...enhud_nonhp_default_supply_calibrated.json | 2 +- .../electric/coned_default_calibrated.json | 2 +- .../coned_default_supply_calibrated.json | 2 +- .../coned_hp_seasonalTOU_flex_calibrated.json | 2 +- .../coned_nonhp_default_calibrated.json | 2 +- ...coned_nonhp_default_supply_calibrated.json | 2 +- .../electric/nimo_default_calibrated.json | 2 +- .../nimo_default_supply_calibrated.json | 2 +- .../nimo_hp_seasonalTOU_flex_calibrated.json | 2 +- .../nimo_nonhp_default_calibrated.json | 2 +- .../nimo_nonhp_default_supply_calibrated.json | 2 +- .../electric/nyseg_default_calibrated.json | 2 +- .../nyseg_default_supply_calibrated.json | 2 +- .../nyseg_nonhp_default_calibrated.json | 2 +- ...nyseg_nonhp_default_supply_calibrated.json | 2 +- .../electric/or_default_calibrated.json | 2 +- .../or_default_supply_calibrated.json | 2 +- .../electric/or_hp_seasonalTOU_flex.json | 2 +- .../or_hp_seasonalTOU_flex_calibrated.json | 2 +- .../or_hp_seasonalTOU_flex_supply.json | 2 +- ...hp_seasonalTOU_flex_supply_calibrated.json | 2 +- .../electric/or_nonhp_default_calibrated.json | 2 +- .../or_nonhp_default_supply_calibrated.json | 2 +- .../electric/psegli_default_calibrated.json | 2 +- .../psegli_default_supply_calibrated.json | 2 +- ...psegli_hp_seasonalTOU_flex_calibrated.json | 2 +- ...hp_seasonalTOU_flex_supply_calibrated.json | 2 +- .../psegli_nonhp_default_calibrated.json | 2 +- ...segli_nonhp_default_supply_calibrated.json | 2 +- .../electric/rge_default_calibrated.json | 2 +- .../rge_default_supply_calibrated.json | 2 +- .../electric/rge_hp_seasonalTOU_flex.json | 2 +- .../rge_hp_seasonalTOU_flex_calibrated.json | 2 +- .../rge_hp_seasonalTOU_flex_supply.json | 2 +- ...hp_seasonalTOU_flex_supply_calibrated.json | 2 +- .../rge_nonhp_default_calibrated.json | 2 +- .../rge_nonhp_default_supply_calibrated.json | 2 +- .../rev_requirement/rie_hp_vs_nonhp.yaml | 20 +- .../ri/config/scenarios/scenarios_rie.yaml | 8 + .../electric/rie_hp_flat_calibrated.json | 2 +- .../rie_hp_flat_supply_calibrated.json | 2 +- rate_design/hp_rates/run_scenario.py | 6 + tests/test_scenario_config.py | 26 +- tests/test_subclass_rr.py | 251 +++++++++++++-- utils/mid/compute_flat_discount_inputs.py | 3 +- utils/mid/compute_subclass_rr.py | 198 +++++++----- utils/mid/patches.py | 287 ++++++++++++++++++ utils/post/build_master_bat.py | 121 ++++---- utils/pre/create_scenario_yamls.py | 1 - utils/scenario_config.py | 86 ++++-- 68 files changed, 978 insertions(+), 299 deletions(-) diff --git a/rate_design/hp_rates/Justfile b/rate_design/hp_rates/Justfile index ab2742a1..29626e2b 100644 --- a/rate_design/hp_rates/Justfile +++ b/rate_design/hp_rates/Justfile @@ -610,7 +610,7 @@ run-17: echo ">> run-17: resolved run-1 output → ${run1_dir}" >&2 just compute-flat-discount-inputs "${run1_dir}" \ "{{ path_resstock_release }}" "{{ state_upper }}" "{{ upgrade }}" \ - "BAT_percustomer" "" \ + "BAT_epmc" "" \ "{{ path_tariffs_electric }}/{{ utility }}_{{ base_tariff_pattern }}_calibrated.json" just create-flat-discount-tariff \ "{{ path_tariffs_electric }}/{{ utility }}_{{ base_tariff_pattern }}_calibrated.json" \ @@ -626,7 +626,7 @@ run-18: echo ">> run-18: resolved run-2 output → ${run2_dir}" >&2 just compute-flat-discount-inputs "${run2_dir}" \ "{{ path_resstock_release }}" "{{ state_upper }}" "{{ upgrade }}" \ - "BAT_percustomer" "" \ + "BAT_epmc" "" \ "{{ path_tariffs_electric }}/{{ utility }}_{{ base_tariff_pattern }}_supply_calibrated.json" just create-flat-discount-tariff \ "{{ path_tariffs_electric }}/{{ utility }}_{{ base_tariff_pattern }}_supply_calibrated.json" \ diff --git a/rate_design/hp_rates/ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml index e6cfa3d9..f96ca992 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml @@ -1,15 +1,17 @@ utility: cenhud group_col: has_hp -cross_subsidy_col: BAT_percustomer source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/cenhud/ny_20260323a_r1-16/20260323_201320_ny_cenhud_run1_up00_precalc__default total_delivery_revenue_requirement: 418151641.73 total_delivery_and_supply_revenue_requirement: 646688375.32 subclass_revenue_requirements: - non-hp: - delivery: 401392812.56143004 - supply: 209823391.60389036 - total: 611216204.1653204 - hp: - delivery: 16758829.168569993 - supply: 18713341.9861097 - total: 35472171.15467969 + percustomer: + non-hp: + delivery: 401392812.56143004 + supply: 209823391.60389036 + total: 611216204.1653204 + hp: + delivery: 16758829.168569993 + supply: 18713341.9861097 + total: 35472171.15467969 +allocation_methods: + - percustomer diff --git a/rate_design/hp_rates/ny/config/rev_requirement/coned_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/coned_hp_vs_nonhp.yaml index 3c6c37a0..3e1df225 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/coned_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/coned_hp_vs_nonhp.yaml @@ -1,15 +1,17 @@ utility: coned group_col: has_hp -cross_subsidy_col: BAT_percustomer source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/coned/ny_20260323a_r1-16/20260324_010700_ny_coned_run1_up00_precalc__default total_delivery_revenue_requirement: 3287782738.4 total_delivery_and_supply_revenue_requirement: 4894675585.19 subclass_revenue_requirements: - non-hp: - delivery: 3208744948.2210865 - supply: 1544462515.047821 - total: 4753207463.268908 - hp: - delivery: 79037790.17891257 - supply: 62430331.74217631 - total: 141468121.92108887 + percustomer: + non-hp: + delivery: 3208744948.2210865 + supply: 1544462515.047821 + total: 4753207463.268908 + hp: + delivery: 79037790.17891257 + supply: 62430331.74217631 + total: 141468121.92108887 +allocation_methods: + - percustomer diff --git a/rate_design/hp_rates/ny/config/rev_requirement/nimo_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/nimo_hp_vs_nonhp.yaml index 05e7675b..771d897c 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/nimo_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/nimo_hp_vs_nonhp.yaml @@ -1,15 +1,17 @@ utility: nimo group_col: has_hp -cross_subsidy_col: BAT_percustomer source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/nimo/ny_20260323a_r1-16/20260323_212709_ny_nimo_run1_up00_precalc__default total_delivery_revenue_requirement: 1419812826.56 total_delivery_and_supply_revenue_requirement: 2572877123.2 subclass_revenue_requirements: - non-hp: - delivery: 1371379962.664701 - supply: 1077043154.9046779 - total: 2448423117.569379 - hp: - delivery: 48432863.89529973 - supply: 76021141.73532248 - total: 124454005.63062221 + percustomer: + non-hp: + delivery: 1371379962.664701 + supply: 1077043154.9046779 + total: 2448423117.569379 + hp: + delivery: 48432863.89529973 + supply: 76021141.73532248 + total: 124454005.63062221 +allocation_methods: + - percustomer diff --git a/rate_design/hp_rates/ny/config/rev_requirement/nyseg_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/nyseg_hp_vs_nonhp.yaml index 93006f3d..601f06cd 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/nyseg_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/nyseg_hp_vs_nonhp.yaml @@ -1,15 +1,17 @@ utility: nyseg group_col: has_hp -cross_subsidy_col: BAT_percustomer source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/nyseg/ny_20260323a_r1-16/20260323_204555_ny_nyseg_run1_up00_precalc__default total_delivery_revenue_requirement: 913708327.54 total_delivery_and_supply_revenue_requirement: 1617781931.62 subclass_revenue_requirements: - non-hp: - delivery: 882168118.8739239 - supply: 660518330.1133512 - total: 1542686448.9872751 - hp: - delivery: 31540208.666075975 - supply: 43555273.96664974 - total: 75095482.63272572 + percustomer: + non-hp: + delivery: 882168118.8739239 + supply: 660518330.1133512 + total: 1542686448.9872751 + hp: + delivery: 31540208.666075975 + supply: 43555273.96664974 + total: 75095482.63272572 +allocation_methods: + - percustomer diff --git a/rate_design/hp_rates/ny/config/rev_requirement/or_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/or_hp_vs_nonhp.yaml index cbff036a..1955554b 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/or_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/or_hp_vs_nonhp.yaml @@ -1,15 +1,17 @@ utility: or group_col: has_hp -cross_subsidy_col: BAT_percustomer source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/or/ny_20260323a_r1-16/20260323_200011_ny_or_run1_up00_precalc__default total_delivery_revenue_requirement: 263364257.0 total_delivery_and_supply_revenue_requirement: 431562114.21 subclass_revenue_requirements: - non-hp: - delivery: 254976490.86444885 - supply: 158986971.95407706 - total: 413963462.8185259 - hp: - delivery: 8387766.13555118 - supply: 9210885.255922925 - total: 17598651.391474105 + percustomer: + non-hp: + delivery: 254976490.86444885 + supply: 158986971.95407706 + total: 413963462.8185259 + hp: + delivery: 8387766.13555118 + supply: 9210885.255922925 + total: 17598651.391474105 +allocation_methods: + - percustomer diff --git a/rate_design/hp_rates/ny/config/rev_requirement/psegli_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/psegli_hp_vs_nonhp.yaml index 88f620ea..0eec6d45 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/psegli_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/psegli_hp_vs_nonhp.yaml @@ -1,15 +1,17 @@ utility: psegli group_col: has_hp -cross_subsidy_col: BAT_percustomer source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/psegli/ny_20260323a_r1-16/20260323_235231_ny_psegli_run1_up00_precalc__default total_delivery_revenue_requirement: 1167061663.64 total_delivery_and_supply_revenue_requirement: 2152328483.56 subclass_revenue_requirements: - non-hp: - delivery: 1150589335.0526578 - supply: 959472648.3151505 - total: 2110061983.3678083 - hp: - delivery: 16472328.587342722 - supply: 25794171.604849398 - total: 42266500.19219212 + percustomer: + non-hp: + delivery: 1150589335.0526578 + supply: 959472648.3151505 + total: 2110061983.3678083 + hp: + delivery: 16472328.587342722 + supply: 25794171.604849398 + total: 42266500.19219212 +allocation_methods: + - percustomer diff --git a/rate_design/hp_rates/ny/config/rev_requirement/rge_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/rge_hp_vs_nonhp.yaml index 5477f257..9920e6fe 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/rge_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/rge_hp_vs_nonhp.yaml @@ -1,15 +1,17 @@ utility: rge group_col: has_hp -cross_subsidy_col: BAT_percustomer source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/rge/ny_20260323a_r1-16/20260323_202742_ny_rge_run1_up00_precalc__default total_delivery_revenue_requirement: 327173904.04 total_delivery_and_supply_revenue_requirement: 601019871.4 subclass_revenue_requirements: - non-hp: - delivery: 312450301.38605285 - supply: 249035038.35532916 - total: 561485339.741382 - hp: - delivery: 14723602.653947193 - supply: 24810929.004670765 - total: 39534531.65861796 + percustomer: + non-hp: + delivery: 312450301.38605285 + supply: 249035038.35532916 + total: 561485339.741382 + hp: + delivery: 14723602.653947193 + supply: 24810929.004670765 + total: 39534531.65861796 +allocation_methods: + - percustomer diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml index 822df69b..5f110dee 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml @@ -140,6 +140,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 6: run_name: ny_cenhud_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -169,6 +170,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 7: run_name: ny_cenhud_run7_up02_default__hp_seasonal state: NY @@ -254,6 +256,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 10: run_name: ny_cenhud_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -283,6 +286,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 11: run_name: ny_cenhud_run11_up02_default__hp_seasonalTOU state: NY @@ -368,6 +372,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 14: run_name: ny_cenhud_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,6 +402,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 15: run_name: ny_cenhud_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml index 2906b563..535e39ea 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml @@ -140,6 +140,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 6: run_name: ny_coned_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -169,6 +170,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 7: run_name: ny_coned_run7_up02_default__hp_seasonal state: NY @@ -254,6 +256,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 10: run_name: ny_coned_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -283,6 +286,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 11: run_name: ny_coned_run11_up02_default__hp_seasonalTOU state: NY @@ -368,6 +372,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 14: run_name: ny_coned_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,6 +402,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 15: run_name: ny_coned_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml index fa11460a..37a53bda 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml @@ -140,6 +140,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 6: run_name: ny_nimo_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -169,6 +170,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 7: run_name: ny_nimo_run7_up02_default__hp_seasonal state: NY @@ -254,6 +256,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 10: run_name: ny_nimo_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -283,6 +286,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 11: run_name: ny_nimo_run11_up02_default__hp_seasonalTOU state: NY @@ -368,6 +372,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 14: run_name: ny_nimo_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,6 +402,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 15: run_name: ny_nimo_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml index d9dc213c..a3a86c3a 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml @@ -140,6 +140,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 6: run_name: ny_nyseg_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -169,6 +170,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 7: run_name: ny_nyseg_run7_up02_default__hp_seasonal state: NY @@ -254,6 +256,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 10: run_name: ny_nyseg_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -283,6 +286,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 11: run_name: ny_nyseg_run11_up02_default__hp_seasonalTOU state: NY @@ -368,6 +372,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 14: run_name: ny_nyseg_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,6 +402,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 15: run_name: ny_nyseg_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml index a0a63682..f16bbcd8 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml @@ -140,6 +140,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 6: run_name: ny_or_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -169,6 +170,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 7: run_name: ny_or_run7_up02_default__hp_seasonal state: NY @@ -254,6 +256,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 10: run_name: ny_or_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -283,6 +286,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 11: run_name: ny_or_run11_up02_default__hp_seasonalTOU state: NY @@ -368,6 +372,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 14: run_name: ny_or_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,6 +402,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 15: run_name: ny_or_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml index 0c224780..5aa8c7cc 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml @@ -140,6 +140,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 6: run_name: ny_psegli_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -169,6 +170,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 7: run_name: ny_psegli_run7_up02_default__hp_seasonal state: NY @@ -254,6 +256,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 10: run_name: ny_psegli_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -283,6 +286,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 11: run_name: ny_psegli_run11_up02_default__hp_seasonalTOU state: NY @@ -368,6 +372,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 14: run_name: ny_psegli_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,6 +402,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 15: run_name: ny_psegli_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml index 11bb7b11..02da1961 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml @@ -140,6 +140,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 6: run_name: ny_rge_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -169,6 +170,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 7: run_name: ny_rge_run7_up02_default__hp_seasonal state: NY @@ -254,6 +256,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 10: run_name: ny_rge_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -283,6 +286,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation: percustomer 11: run_name: ny_rge_run11_up02_default__hp_seasonalTOU state: NY @@ -368,6 +372,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 14: run_name: ny_rge_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,6 +402,7 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation: percustomer 15: run_name: ny_rge_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_default_calibrated.json index a58a8505..1325d09d 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_default_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_default_supply_calibrated.json index f47a7bb4..a6cab147 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_default_supply_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_nonhp_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_nonhp_default_calibrated.json index 0e935973..a29b7adc 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_nonhp_default_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_nonhp_default_supply_calibrated.json index bfdf18a0..a110c118 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_nonhp_default_supply_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/coned_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/coned_default_calibrated.json index f1766294..e6c9855e 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/coned_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/coned_default_calibrated.json @@ -671,4 +671,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/coned_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/coned_default_supply_calibrated.json index e24a2702..2795c7e3 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/coned_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/coned_default_supply_calibrated.json @@ -801,4 +801,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/coned_hp_seasonalTOU_flex_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/coned_hp_seasonalTOU_flex_calibrated.json index 28008859..5578cb9a 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/coned_hp_seasonalTOU_flex_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/coned_hp_seasonalTOU_flex_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/coned_nonhp_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/coned_nonhp_default_calibrated.json index 45a3488a..9d85789d 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/coned_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/coned_nonhp_default_calibrated.json @@ -671,4 +671,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/coned_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/coned_nonhp_default_supply_calibrated.json index f1ab4134..8c367c0a 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/coned_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/coned_nonhp_default_supply_calibrated.json @@ -801,4 +801,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_default_calibrated.json index e129c119..93980a62 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_default_calibrated.json @@ -680,4 +680,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_default_supply_calibrated.json index 862ae3bc..029ac30e 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_default_supply_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_hp_seasonalTOU_flex_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_hp_seasonalTOU_flex_calibrated.json index ed85dbe1..16b54d70 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_hp_seasonalTOU_flex_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_hp_seasonalTOU_flex_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_nonhp_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_nonhp_default_calibrated.json index cee0dee8..3222605d 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_nonhp_default_calibrated.json @@ -680,4 +680,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_nonhp_default_supply_calibrated.json index 2cad7f1f..af5927f0 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nimo_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nimo_nonhp_default_supply_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_default_calibrated.json index 0163c0fd..f817be2a 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_default_calibrated.json @@ -687,4 +687,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_default_supply_calibrated.json index 92d52ecd..f57cabe8 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_default_supply_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_nonhp_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_nonhp_default_calibrated.json index dc4a0014..f5f4e2c6 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_nonhp_default_calibrated.json @@ -687,4 +687,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_nonhp_default_supply_calibrated.json index 40d46dc8..40ab3a5c 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/nyseg_nonhp_default_supply_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/or_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/or_default_calibrated.json index b489f356..835c1c43 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/or_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/or_default_calibrated.json @@ -671,4 +671,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/or_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/or_default_supply_calibrated.json index 72ab1174..c7059bb3 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/or_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/or_default_supply_calibrated.json @@ -801,4 +801,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex.json b/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex.json index 3a657be9..eda4c16f 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex.json @@ -675,4 +675,4 @@ "demandrateunit": "kW" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_calibrated.json index fa734831..071e260f 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_supply.json b/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_supply.json index 6f8a798f..4e531e34 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_supply.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_supply.json @@ -675,4 +675,4 @@ "demandrateunit": "kW" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_supply_calibrated.json index 451b3955..1083f0b7 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/or_hp_seasonalTOU_flex_supply_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/or_nonhp_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/or_nonhp_default_calibrated.json index ce17941b..b303c38a 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/or_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/or_nonhp_default_calibrated.json @@ -671,4 +671,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/or_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/or_nonhp_default_supply_calibrated.json index dec0102b..b4e0ddc6 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/or_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/or_nonhp_default_supply_calibrated.json @@ -801,4 +801,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_default_calibrated.json index 6d55a9ec..cddb1f8f 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_default_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_default_supply_calibrated.json index bf5e86a4..4a27d375 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_default_supply_calibrated.json @@ -813,4 +813,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_hp_seasonalTOU_flex_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_hp_seasonalTOU_flex_calibrated.json index 956d1194..8a5e1966 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_hp_seasonalTOU_flex_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_hp_seasonalTOU_flex_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_hp_seasonalTOU_flex_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_hp_seasonalTOU_flex_supply_calibrated.json index 76ad9fd8..60c79cca 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_hp_seasonalTOU_flex_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_hp_seasonalTOU_flex_supply_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_nonhp_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_nonhp_default_calibrated.json index 82a0c6fd..adaaad3b 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_nonhp_default_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_nonhp_default_supply_calibrated.json index 538a32d5..90302a25 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/psegli_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/psegli_nonhp_default_supply_calibrated.json @@ -813,4 +813,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_default_calibrated.json index fc2734df..366d182e 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/rge_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_default_calibrated.json @@ -666,4 +666,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_default_supply_calibrated.json index 8c5e6fe6..40ef5d3e 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/rge_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_default_supply_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex.json index 61f9969a..0e1d5b42 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex.json @@ -675,4 +675,4 @@ "demandrateunit": "kW" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_calibrated.json index e89d886f..7ef85106 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_supply.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_supply.json index 87940d02..40ec9f83 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_supply.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_supply.json @@ -675,4 +675,4 @@ "demandrateunit": "kW" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_supply_calibrated.json index 53a998e4..6f6b3b72 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_seasonalTOU_flex_supply_calibrated.json @@ -673,4 +673,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_nonhp_default_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_nonhp_default_calibrated.json index d368246a..ad7ec3a7 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/rge_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_nonhp_default_calibrated.json @@ -666,4 +666,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_nonhp_default_supply_calibrated.json index b86c5254..fa6e814b 100644 --- a/rate_design/hp_rates/ny/config/tariffs/electric/rge_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_nonhp_default_supply_calibrated.json @@ -729,4 +729,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml index 39fbd4bc..10463bdd 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml @@ -1,15 +1,17 @@ utility: rie group_col: has_hp -cross_subsidy_col: BAT_percustomer source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260324_r1-20_fixedcharges_hpflat/20260324_233238_ri_rie_run1_up00_precalc__default total_delivery_revenue_requirement: 542556587.55 total_delivery_and_supply_revenue_requirement: 955506957.66 subclass_revenue_requirements: - non-hp: - delivery: 531018474.4165694 - supply: 394774670.83571464 - total: 925793145.252284 - hp: - delivery: 11538113.133430583 - supply: 18175699.274285078 - total: 29713812.407715663 + percustomer: + non-hp: + delivery: 531018474.4165694 + supply: 394774670.83571464 + total: 925793145.252284 + hp: + delivery: 11538113.133430583 + supply: 18175699.274285078 + total: 29713812.407715663 +allocation_methods: + - percustomer diff --git a/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml b/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml index e4860fad..5aeb9ea1 100644 --- a/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml +++ b/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml @@ -133,6 +133,7 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -162,6 +163,7 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -247,6 +249,7 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -276,6 +279,7 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -361,6 +365,7 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -390,6 +395,7 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -475,6 +481,7 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: epmc path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -504,6 +511,7 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: epmc path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json index 19ebb1ad..313ebcca 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json @@ -652,4 +652,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json index 170a380f..df35118c 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json @@ -652,4 +652,4 @@ "minchargeunits": "$/month" } ] -} \ No newline at end of file +} diff --git a/rate_design/hp_rates/run_scenario.py b/rate_design/hp_rates/run_scenario.py index d5249b1c..987542ea 100644 --- a/rate_design/hp_rates/run_scenario.py +++ b/rate_design/hp_rates/run_scenario.py @@ -99,6 +99,7 @@ class ScenarioSettings: rr_total: float subclass_rr: dict[str, float] | None run_includes_subclasses: bool + residual_allocation: str | None path_electric_utility_stats: str | Path path_supply_energy_mc: str | Path path_supply_capacity_mc: str | Path @@ -241,13 +242,17 @@ def _build_settings_from_yaml_run( _require_value(run, "run_includes_subclasses"), "run_includes_subclasses", ) + residual_allocation: str | None = run.get("residual_allocation") rr_config: RevenueRequirementConfig = _parse_utility_revenue_requirement( _require_value(run, "utility_revenue_requirement"), path_config, raw_path_tariffs_electric, add_supply=run_includes_supply, run_includes_subclasses=run_includes_subclasses, + residual_allocation=residual_allocation, ) + if residual_allocation: + log.info("Residual allocation method: %s", residual_allocation) path_tariff_maps_gas = _resolve_path( str(_require_value(run, "path_tariff_maps_gas")), path_config, @@ -310,6 +315,7 @@ def _build_settings_from_yaml_run( rr_total=rr_config.rr_total, subclass_rr=rr_config.subclass_rr, run_includes_subclasses=rr_config.run_includes_subclasses, + residual_allocation=rr_config.residual_allocation, path_electric_utility_stats=path_electric_utility_stats, year_run=year_run, year_dollar_conversion=year_dollar_conversion, diff --git a/tests/test_scenario_config.py b/tests/test_scenario_config.py index 05f83b5e..c4ca7846 100644 --- a/tests/test_scenario_config.py +++ b/tests/test_scenario_config.py @@ -34,7 +34,7 @@ def test_scalar_delivery_only(self) -> None: run_includes_subclasses=False, ) assert isinstance(result, RevenueRequirementConfig) - assert result.rr_total == pytest.approx(416_193_684.03) + assert result.rr_total == pytest.approx(418_151_641.73) assert result.subclass_rr is None assert result.run_includes_subclasses is False @@ -49,7 +49,7 @@ def test_scalar_delivery_plus_supply(self) -> None: add_supply=True, run_includes_subclasses=False, ) - assert result.rr_total == pytest.approx(624_942_307.81) + assert result.rr_total == pytest.approx(646_688_375.32) assert result.subclass_rr is None def test_subclass_delivery_only(self) -> None: @@ -63,6 +63,7 @@ def test_subclass_delivery_only(self) -> None: }, add_supply=False, run_includes_subclasses=True, + residual_allocation="percustomer", ) assert result.run_includes_subclasses is True assert result.subclass_rr is not None @@ -84,6 +85,7 @@ def test_subclass_delivery_plus_supply(self) -> None: }, add_supply=True, run_includes_subclasses=True, + residual_allocation="percustomer", ) assert result.subclass_rr is not None assert set(result.subclass_rr.keys()) == { @@ -104,6 +106,7 @@ def test_subclass_flex_delivery_only(self) -> None: }, add_supply=False, run_includes_subclasses=True, + residual_allocation="percustomer", ) assert result.subclass_rr is not None assert set(result.subclass_rr.keys()) == { @@ -145,6 +148,7 @@ def test_subclass_run_without_subclass_data_raises(self, tmp_path: Path) -> None }, add_supply=False, run_includes_subclasses=True, + residual_allocation="percustomer", ) def test_nested_subclass_format(self, tmp_path: Path) -> None: @@ -156,14 +160,23 @@ def test_nested_subclass_format(self, tmp_path: Path) -> None: "total_delivery_revenue_requirement": 1000.0, "total_delivery_and_supply_revenue_requirement": 1500.0, "subclass_revenue_requirements": { - "hp": {"delivery": 300.0, "supply": 200.0, "total": 500.0}, - "non-hp": {"delivery": 700.0, "supply": 300.0, "total": 1000.0}, + "percustomer": { + "hp": { + "delivery": 300.0, + "supply": 200.0, + "total": 500.0, + }, + "non-hp": { + "delivery": 700.0, + "supply": 300.0, + "total": 1000.0, + }, + }, }, } ), encoding="utf-8", ) - # Delivery-only result = _parse_utility_revenue_requirement( str(rr_yaml), tmp_path, @@ -173,11 +186,11 @@ def test_nested_subclass_format(self, tmp_path: Path) -> None: }, add_supply=False, run_includes_subclasses=True, + residual_allocation="percustomer", ) assert result.rr_total == pytest.approx(1000.0) assert result.subclass_rr == {"hp_tariff": 300.0, "nonhp_tariff": 700.0} - # With supply result_supply = _parse_utility_revenue_requirement( str(rr_yaml), tmp_path, @@ -187,6 +200,7 @@ def test_nested_subclass_format(self, tmp_path: Path) -> None: }, add_supply=True, run_includes_subclasses=True, + residual_allocation="percustomer", ) assert result_supply.rr_total == pytest.approx(1500.0) assert result_supply.subclass_rr == { diff --git a/tests/test_subclass_rr.py b/tests/test_subclass_rr.py index 5e9708d7..3ae5e933 100644 --- a/tests/test_subclass_rr.py +++ b/tests/test_subclass_rr.py @@ -89,6 +89,7 @@ def _write_sample_run_dir(tmp_path: Path) -> Path: "bldg_id": [1, 2, 3, 4], "BAT_percustomer": [3.0, 20.0, 4.0, 40.0], "BAT_vol": [1.0, 2.0, 3.0, 4.0], + "BAT_epmc": [2.5, 18.0, 3.5, 36.0], } ).write_csv(run_dir / "cross_subsidization" / "cross_subsidization_BAT_values.csv") @@ -163,6 +164,14 @@ def _write_sample_resstock_loads_dir(tmp_path: Path) -> Path: "resistance": (400.0, 4.0, 396.0), }, ), + ( + "has_hp", + "BAT_epmc", + { + "false": (600.0, 54.0, 546.0), + "true": (400.0, 6.0, 394.0), + }, + ), ], ) def test_compute_subclass_rr_for_multiple_groupings( @@ -172,9 +181,10 @@ def test_compute_subclass_rr_for_multiple_groupings( expected: dict[str, tuple[float, float, float]], ) -> None: run_dir = _write_sample_run_dir(tmp_path) - breakdown = compute_subclass_rr( - run_dir, group_col=group_col, cross_subsidy_col=cross_subsidy_col + results = compute_subclass_rr( + run_dir, group_col=group_col, cross_subsidy_cols=cross_subsidy_col ) + breakdown = results[cross_subsidy_col] assert breakdown.height == len(expected) for subclass, (sum_bills, sum_cross_subsidy, rr) in expected.items(): @@ -184,6 +194,25 @@ def test_compute_subclass_rr_for_multiple_groupings( assert row["revenue_requirement"][0] == pytest.approx(rr) +def test_compute_subclass_rr_multi_allocation(tmp_path: Path) -> None: + """Compute both percustomer and EPMC in a single call.""" + run_dir = _write_sample_run_dir(tmp_path) + results = compute_subclass_rr( + run_dir, cross_subsidy_cols=("BAT_percustomer", "BAT_epmc") + ) + assert "BAT_percustomer" in results + assert "BAT_epmc" in results + + pc = results["BAT_percustomer"] + ep = results["BAT_epmc"] + assert pc.height == 2 + assert ep.height == 2 + + pc_hp = pc.filter(pl.col("subclass") == "true") + ep_hp = ep.filter(pl.col("subclass") == "true") + assert pc_hp["revenue_requirement"][0] != ep_hp["revenue_requirement"][0] + + def test_compute_subclass_rr_missing_annual_rows_raises(tmp_path: Path) -> None: run_dir = tmp_path / "run" (run_dir / "bills").mkdir(parents=True) @@ -200,7 +229,7 @@ def test_compute_subclass_rr_missing_annual_rows_raises(tmp_path: Path) -> None: ).write_csv(run_dir / "customer_metadata.csv") with pytest.raises(ValueError, match="Missing annual target bills"): - compute_subclass_rr(run_dir) + compute_subclass_rr(run_dir, cross_subsidy_cols="BAT_percustomer") def test_load_run_fields_from_scenario_config(tmp_path: Path) -> None: @@ -252,7 +281,8 @@ def test_compute_subclass_rr_applies_weights(tmp_path: Path) -> None: } ).write_csv(run_dir / "customer_metadata.csv") - breakdown = compute_subclass_rr(run_dir) + results = compute_subclass_rr(run_dir, cross_subsidy_cols="BAT_percustomer") + breakdown = results["BAT_percustomer"] hp = breakdown.filter(pl.col("subclass") == "true") nonhp = breakdown.filter(pl.col("subclass") == "false") assert hp["sum_bills"][0] == pytest.approx(100.0) @@ -288,12 +318,12 @@ def _write_sample_run2_dir(tmp_path: Path) -> Path: } ).write_csv(run_dir / "bills" / "elec_bills_year_target.csv") - # Same BAT values — the cross-subsidy allocation is independent pl.DataFrame( { "bldg_id": [1, 2, 3, 4], "BAT_percustomer": [3.0, 20.0, 4.0, 40.0], "BAT_vol": [1.0, 2.0, 3.0, 4.0], + "BAT_epmc": [2.5, 18.0, 3.5, 36.0], } ).write_csv(run_dir / "cross_subsidization" / "cross_subsidization_BAT_values.csv") @@ -312,24 +342,27 @@ def test_write_revenue_requirement_yamls_two_runs(tmp_path: Path) -> None: """Verify the nested YAML: delivery from run 1, total from run 2, supply = diff.""" run1_dir = _write_sample_run_dir(tmp_path) run2_dir = _write_sample_run2_dir(tmp_path) - delivery_breakdown = compute_subclass_rr(run1_dir) - total_breakdown = compute_subclass_rr(run2_dir) + delivery_breakdowns = compute_subclass_rr( + run1_dir, cross_subsidy_cols="BAT_percustomer" + ) + total_breakdowns = compute_subclass_rr( + run2_dir, cross_subsidy_cols="BAT_percustomer" + ) differentiated_yaml = tmp_path / "config/rev_requirement/rie_hp_vs_nonhp.yaml" default_yaml = tmp_path / "config/rev_requirement/rie.yaml" gv_map = {"true": "hp", "false": "non-hp"} out_diff, out_default = _write_revenue_requirement_yamls( - delivery_breakdown, + delivery_breakdowns, run_dir=run1_dir, group_col="has_hp", - cross_subsidy_col="BAT_percustomer", utility="rie", default_revenue_requirement=241869601.0, differentiated_yaml_path=differentiated_yaml, default_yaml_path=default_yaml, group_value_to_subclass=gv_map, - total_breakdown=total_breakdown, + total_breakdowns=total_breakdowns, total_delivery_rr=933.0, total_delivery_and_supply_rr=1133.0, ) @@ -347,30 +380,36 @@ def test_write_revenue_requirement_yamls_two_runs(tmp_path: Path) -> None: ) sr = diff_data["subclass_revenue_requirements"] - assert "hp" in sr - assert "non-hp" in sr + assert "percustomer" in sr + pc = sr["percustomer"] # Run 1 delivery: hp=400-7=393, non-hp=600-60=540 # Run 2 total: hp=500-7=493, non-hp=700-60=640 (each bldg +50, same BAT) # Supply = total - delivery - assert sr["hp"]["delivery"] == pytest.approx(393.0) - assert sr["hp"]["total"] == pytest.approx(493.0) - assert sr["hp"]["supply"] == pytest.approx(100.0) - assert sr["non-hp"]["delivery"] == pytest.approx(540.0) - assert sr["non-hp"]["total"] == pytest.approx(640.0) - assert sr["non-hp"]["supply"] == pytest.approx(100.0) + assert pc["hp"]["delivery"] == pytest.approx(393.0) + assert pc["hp"]["total"] == pytest.approx(493.0) + assert pc["hp"]["supply"] == pytest.approx(100.0) + assert pc["non-hp"]["delivery"] == pytest.approx(540.0) + assert pc["non-hp"]["total"] == pytest.approx(640.0) + assert pc["non-hp"]["supply"] == pytest.approx(100.0) def test_write_revenue_requirement_yamls_round_trip(tmp_path: Path) -> None: """Delivery sums match total_delivery_rr, total == delivery + supply per subclass.""" run1_dir = _write_sample_run_dir(tmp_path) run2_dir = _write_sample_run2_dir(tmp_path) - delivery_breakdown = compute_subclass_rr(run1_dir) - total_breakdown = compute_subclass_rr(run2_dir) + delivery_breakdowns = compute_subclass_rr( + run1_dir, cross_subsidy_cols="BAT_percustomer" + ) + total_breakdowns = compute_subclass_rr( + run2_dir, cross_subsidy_cols="BAT_percustomer" + ) differentiated_yaml = tmp_path / "config/rev_requirement/test_hp_vs_nonhp.yaml" default_yaml = tmp_path / "config/rev_requirement/test.yaml" gv_map = {"true": "hp", "false": "non-hp"} + delivery_breakdown = delivery_breakdowns["BAT_percustomer"] + total_breakdown = total_breakdowns["BAT_percustomer"] delivery_total = sum( float(row["revenue_requirement"]) for row in delivery_breakdown.to_dicts() ) @@ -379,16 +418,15 @@ def test_write_revenue_requirement_yamls_round_trip(tmp_path: Path) -> None: ) _write_revenue_requirement_yamls( - delivery_breakdown, + delivery_breakdowns, run_dir=run1_dir, group_col="has_hp", - cross_subsidy_col="BAT_percustomer", utility="test", default_revenue_requirement=0.0, differentiated_yaml_path=differentiated_yaml, default_yaml_path=default_yaml, group_value_to_subclass=gv_map, - total_breakdown=total_breakdown, + total_breakdowns=total_breakdowns, total_delivery_rr=delivery_total, total_delivery_and_supply_rr=total_total, ) @@ -396,18 +434,21 @@ def test_write_revenue_requirement_yamls_round_trip(tmp_path: Path) -> None: data = yaml.safe_load(differentiated_yaml.read_text(encoding="utf-8")) sr = data["subclass_revenue_requirements"] - sum_delivery = sum(v["delivery"] for v in sr.values()) - sum_total = sum(v["total"] for v in sr.values()) + for method_key, method_block in sr.items(): + sum_delivery = sum(v["delivery"] for v in method_block.values()) + sum_total = sum(v["total"] for v in method_block.values()) - assert sum_delivery == pytest.approx(data["total_delivery_revenue_requirement"]) - assert sum_total == pytest.approx( - data["total_delivery_and_supply_revenue_requirement"] - ) + assert sum_delivery == pytest.approx( + data["total_delivery_revenue_requirement"] + ), f"Method {method_key}: delivery sum mismatch" + assert sum_total == pytest.approx( + data["total_delivery_and_supply_revenue_requirement"] + ), f"Method {method_key}: total sum mismatch" - for alias, vals in sr.items(): - assert vals["total"] == pytest.approx(vals["delivery"] + vals["supply"]), ( - f"Subclass {alias}: total != delivery + supply" - ) + for alias, vals in method_block.items(): + assert vals["total"] == pytest.approx(vals["delivery"] + vals["supply"]), ( + f"Method {method_key}, subclass {alias}: total != delivery + supply" + ) def test_parse_group_value_to_subclass() -> None: @@ -1080,3 +1121,147 @@ def test_compute_hp_flat_discount_inputs_raises_when_negative_rate( upgrade=_LOADS_UPGRADE, base_tariff_json_path=tariff_path, ) + + +# ============================================================================= +# Config parsing tests for nested YAML + residual_allocation +# ============================================================================= + + +def test_parse_nested_yaml_percustomer(tmp_path: Path) -> None: + """Nested YAML with residual_allocation=percustomer returns correct values.""" + from utils.scenario_config import _parse_utility_revenue_requirement + + rr_yaml = tmp_path / "rr.yaml" + tariff_dir = tmp_path / "tariffs" + tariff_dir.mkdir() + (tariff_dir / "rie_hp.json").write_text("{}") + (tariff_dir / "rie_nonhp.json").write_text("{}") + + rr_yaml.write_text( + yaml.safe_dump( + { + "total_delivery_revenue_requirement": 1000.0, + "subclass_revenue_requirements": { + "percustomer": { + "hp": {"delivery": 100.0, "supply": 50.0, "total": 150.0}, + "non-hp": {"delivery": 900.0, "supply": 450.0, "total": 1350.0}, + }, + "epmc": { + "hp": {"delivery": 120.0, "supply": 60.0, "total": 180.0}, + "non-hp": {"delivery": 880.0, "supply": 440.0, "total": 1320.0}, + }, + }, + } + ) + ) + + raw_tariffs = {"hp": "tariffs/rie_hp.json", "non-hp": "tariffs/rie_nonhp.json"} + config = _parse_utility_revenue_requirement( + str(rr_yaml), + tmp_path, + raw_tariffs, + add_supply=False, + run_includes_subclasses=True, + residual_allocation="percustomer", + ) + assert config.subclass_rr is not None + assert config.subclass_rr["rie_hp"] == pytest.approx(100.0) + assert config.subclass_rr["rie_nonhp"] == pytest.approx(900.0) + assert config.residual_allocation == "percustomer" + + +def test_parse_nested_yaml_epmc(tmp_path: Path) -> None: + """Nested YAML with residual_allocation=epmc returns EPMC values.""" + from utils.scenario_config import _parse_utility_revenue_requirement + + rr_yaml = tmp_path / "rr.yaml" + tariff_dir = tmp_path / "tariffs" + tariff_dir.mkdir() + (tariff_dir / "rie_hp.json").write_text("{}") + (tariff_dir / "rie_nonhp.json").write_text("{}") + + rr_yaml.write_text( + yaml.safe_dump( + { + "total_delivery_revenue_requirement": 1000.0, + "subclass_revenue_requirements": { + "percustomer": { + "hp": {"delivery": 100.0, "supply": 50.0, "total": 150.0}, + "non-hp": {"delivery": 900.0, "supply": 450.0, "total": 1350.0}, + }, + "epmc": { + "hp": {"delivery": 120.0, "supply": 60.0, "total": 180.0}, + "non-hp": {"delivery": 880.0, "supply": 440.0, "total": 1320.0}, + }, + }, + } + ) + ) + + raw_tariffs = {"hp": "tariffs/rie_hp.json", "non-hp": "tariffs/rie_nonhp.json"} + config = _parse_utility_revenue_requirement( + str(rr_yaml), + tmp_path, + raw_tariffs, + add_supply=False, + run_includes_subclasses=True, + residual_allocation="epmc", + ) + assert config.subclass_rr is not None + assert config.subclass_rr["rie_hp"] == pytest.approx(120.0) + assert config.subclass_rr["rie_nonhp"] == pytest.approx(880.0) + + +def test_parse_nested_yaml_unknown_allocation_raises(tmp_path: Path) -> None: + """Unknown residual_allocation raises ValueError listing available methods.""" + from utils.scenario_config import _parse_utility_revenue_requirement + + rr_yaml = tmp_path / "rr.yaml" + tariff_dir = tmp_path / "tariffs" + tariff_dir.mkdir() + (tariff_dir / "rie_hp.json").write_text("{}") + (tariff_dir / "rie_nonhp.json").write_text("{}") + + rr_yaml.write_text( + yaml.safe_dump( + { + "total_delivery_revenue_requirement": 1000.0, + "subclass_revenue_requirements": { + "percustomer": { + "hp": {"delivery": 100.0, "supply": 50.0, "total": 150.0}, + "non-hp": {"delivery": 900.0, "supply": 450.0, "total": 1350.0}, + }, + }, + } + ) + ) + + raw_tariffs = {"hp": "tariffs/rie_hp.json", "non-hp": "tariffs/rie_nonhp.json"} + with pytest.raises(ValueError, match="residual_allocation='epcm'.*Available"): + _parse_utility_revenue_requirement( + str(rr_yaml), + tmp_path, + raw_tariffs, + add_supply=False, + run_includes_subclasses=True, + residual_allocation="epcm", + ) + + +def test_parse_missing_residual_allocation_raises(tmp_path: Path) -> None: + """Missing residual_allocation for subclass run raises ValueError.""" + from utils.scenario_config import _parse_utility_revenue_requirement + + rr_yaml = tmp_path / "rr.yaml" + rr_yaml.write_text(yaml.safe_dump({"total_delivery_revenue_requirement": 1000.0})) + + with pytest.raises(ValueError, match="residual_allocation is not set"): + _parse_utility_revenue_requirement( + str(rr_yaml), + tmp_path, + {}, + add_supply=False, + run_includes_subclasses=True, + residual_allocation=None, + ) diff --git a/utils/mid/compute_flat_discount_inputs.py b/utils/mid/compute_flat_discount_inputs.py index 5acc04f3..100aa3c3 100644 --- a/utils/mid/compute_flat_discount_inputs.py +++ b/utils/mid/compute_flat_discount_inputs.py @@ -11,6 +11,7 @@ from data.eia.hourly_loads.eia_region_config import get_aws_storage_options from utils.mid.compute_subclass_rr import ( + BAT_METRIC_CHOICES, DEFAULT_BAT_METRIC, DEFAULT_FLAT_OUTPUT_FILENAME, _resolve_path_or_s3, @@ -65,7 +66,7 @@ def main() -> None: parser.add_argument( "--cross-subsidy-col", default=DEFAULT_BAT_METRIC, - choices=("BAT_vol", "BAT_peak", "BAT_percustomer"), + choices=BAT_METRIC_CHOICES, help="BAT column in cross_subsidization_BAT_values.csv to use.", ) parser.add_argument( diff --git a/utils/mid/compute_subclass_rr.py b/utils/mid/compute_subclass_rr.py index 29938307..68d3ccec 100644 --- a/utils/mid/compute_subclass_rr.py +++ b/utils/mid/compute_subclass_rr.py @@ -35,8 +35,15 @@ BLDG_ID_COL = "bldg_id" WEIGHT_COL = "weight" DEFAULT_GROUP_COL = "has_hp" -BAT_METRIC_CHOICES = ("BAT_vol", "BAT_peak", "BAT_percustomer") +BAT_METRIC_CHOICES = ("BAT_vol", "BAT_peak", "BAT_percustomer", "BAT_epmc") DEFAULT_BAT_METRIC = "BAT_percustomer" +SUBCLASS_RR_ALLOCATION_METHODS: tuple[str, ...] = ("BAT_percustomer", "BAT_epmc") +BAT_COL_TO_ALLOCATION_KEY: dict[str, str] = { + "BAT_percustomer": "percustomer", + "BAT_epmc": "epmc", + "BAT_vol": "volumetric", + "BAT_peak": "peak", +} # Output constants GROUP_VALUE_COL = "subclass" @@ -622,22 +629,44 @@ def compute_hp_flat_discount_inputs( def compute_subclass_rr( run_dir: S3Path | Path, group_col: str = DEFAULT_GROUP_COL, - cross_subsidy_col: str = DEFAULT_BAT_METRIC, + cross_subsidy_cols: str | tuple[str, ...] = SUBCLASS_RR_ALLOCATION_METHODS, annual_month: str = ANNUAL_MONTH_VALUE, storage_options: dict[str, str] | None = None, -) -> pl.DataFrame: - """Return subclass revenue requirement breakdown for the selected grouping. +) -> dict[str, pl.DataFrame]: + """Return subclass revenue requirement breakdowns for one or more BAT columns. - Columns: subclass, sum_bills, sum_cross_subsidy, revenue_requirement + Loads bills and BAT CSV once, joins once, then computes a separate + breakdown per BAT column. Returns ``{bat_col: DataFrame}`` where each + DataFrame has columns: subclass, sum_bills, sum_cross_subsidy, + revenue_requirement. + + For backward compat, *cross_subsidy_cols* may be a single string. """ + if isinstance(cross_subsidy_cols, str): + cross_subsidy_cols = (cross_subsidy_cols,) + group_values = _load_group_values(run_dir, group_col, storage_options) bills = _load_annual_target_bills(run_dir, annual_month, storage_options) - cross_sub = _load_cross_subsidy(run_dir, cross_subsidy_col, storage_options) + + bat_select = [pl.col(BLDG_ID_COL).cast(pl.Int64)] + [ + pl.col(col).cast(pl.Float64) for col in cross_subsidy_cols + ] + cross_sub_all = ( + pl.scan_csv( + _csv_path( + run_dir, "cross_subsidization/cross_subsidization_BAT_values.csv" + ), + storage_options=storage_options, + ) + .select(bat_select) + .group_by(BLDG_ID_COL) + .agg([pl.col(col).sum() for col in cross_subsidy_cols]) + ) joined = cast( pl.DataFrame, group_values.join(bills, on=BLDG_ID_COL, how="left") - .join(cross_sub, on=BLDG_ID_COL, how="left") + .join(cross_sub_all, on=BLDG_ID_COL, how="left") .collect(), ) if joined.is_empty(): @@ -652,35 +681,40 @@ def compute_subclass_rr( ) raise ValueError(msg) - nulls_cs = joined.filter(pl.col("cross_subsidy").is_null()).height - if nulls_cs: - msg = f"Missing cross-subsidy values for {nulls_cs} buildings." - raise ValueError(msg) + for col in cross_subsidy_cols: + nulls_cs = joined.filter(pl.col(col).is_null()).height + if nulls_cs: + msg = f"Missing cross-subsidy values for {nulls_cs} buildings in {col}." + raise ValueError(msg) nulls_weight = joined.filter(pl.col(WEIGHT_COL).is_null()).height if nulls_weight: msg = f"Missing sample weights for {nulls_weight} buildings." raise ValueError(msg) - return ( - joined.with_columns( - (pl.col("annual_bill") * pl.col(WEIGHT_COL)).alias("weighted_annual_bill"), - (pl.col("cross_subsidy") * pl.col(WEIGHT_COL)).alias( - "weighted_cross_subsidy" - ), - ) - .group_by(GROUP_VALUE_COL) - .agg( - pl.col("weighted_annual_bill").sum().alias("sum_bills"), - pl.col("weighted_cross_subsidy").sum().alias("sum_cross_subsidy"), - ) - .with_columns( - (pl.col("sum_bills") - pl.col("sum_cross_subsidy")).alias( - "revenue_requirement" + results: dict[str, pl.DataFrame] = {} + for col in cross_subsidy_cols: + results[col] = ( + joined.with_columns( + (pl.col("annual_bill") * pl.col(WEIGHT_COL)).alias( + "weighted_annual_bill" + ), + (pl.col(col) * pl.col(WEIGHT_COL)).alias("weighted_cross_subsidy"), + ) + .group_by(GROUP_VALUE_COL) + .agg( + pl.col("weighted_annual_bill").sum().alias("sum_bills"), + pl.col("weighted_cross_subsidy").sum().alias("sum_cross_subsidy"), + ) + .with_columns( + (pl.col("sum_bills") - pl.col("sum_cross_subsidy")).alias( + "revenue_requirement" + ) ) + .sort(GROUP_VALUE_COL) ) - .sort(GROUP_VALUE_COL) - ) + + return results def _load_run_from_scenario_config( @@ -738,55 +772,71 @@ def _load_run_fields( def _write_revenue_requirement_yamls( - delivery_breakdown: pl.DataFrame, + delivery_breakdowns: dict[str, pl.DataFrame], run_dir: S3Path | Path, group_col: str, - cross_subsidy_col: str, utility: str, default_revenue_requirement: float, differentiated_yaml_path: Path, default_yaml_path: Path, *, group_value_to_subclass: dict[str, str] | None = None, - total_breakdown: pl.DataFrame | None = None, + total_breakdowns: dict[str, pl.DataFrame] | None = None, total_delivery_rr: float | None = None, total_delivery_and_supply_rr: float | None = None, ) -> tuple[Path, Path]: - """Write per-subclass revenue requirement YAML. + """Write per-subclass revenue requirement YAML with nested allocation methods. + + *delivery_breakdowns* is ``{bat_col: DataFrame}`` from run 1 (delivery-only). + *total_breakdowns*, when provided, is ``{bat_col: DataFrame}`` from run 2 + (delivery+supply). Supply per subclass is derived as total - delivery. + + Output YAML structure:: - *delivery_breakdown* comes from run 1 (delivery-only BAT). - *total_breakdown*, when provided, comes from run 2 (delivery+supply BAT). - Supply per subclass is derived as total - delivery. + subclass_revenue_requirements: + percustomer: + hp: {delivery: ..., supply: ..., total: ...} + non-hp: {delivery: ..., supply: ..., total: ...} + epmc: + hp: {delivery: ..., supply: ..., total: ...} + non-hp: {delivery: ..., supply: ..., total: ...} """ differentiated_yaml_path.parent.mkdir(parents=True, exist_ok=True) default_yaml_path.parent.mkdir(parents=True, exist_ok=True) gv_map = group_value_to_subclass or {} - total_rr_by_subclass: dict[str, float] = {} - if total_breakdown is not None: - for row in total_breakdown.to_dicts(): - total_rr_by_subclass[str(row["subclass"])] = float( - row["revenue_requirement"] - ) - - subclass_rr: dict[str, dict[str, float]] = {} - for row in delivery_breakdown.to_dicts(): - raw_val = str(row["subclass"]) - alias = gv_map.get(raw_val, raw_val) - delivery = float(row["revenue_requirement"]) - total = total_rr_by_subclass.get(raw_val, delivery) - supply = total - delivery - subclass_rr[alias] = { - "delivery": delivery, - "supply": supply, - "total": total, - } + all_methods_rr: dict[str, dict[str, dict[str, float]]] = {} + + for bat_col, delivery_breakdown in delivery_breakdowns.items(): + method_key = BAT_COL_TO_ALLOCATION_KEY.get(bat_col, bat_col) + + total_rr_by_subclass: dict[str, float] = {} + if total_breakdowns is not None and bat_col in total_breakdowns: + for row in total_breakdowns[bat_col].to_dicts(): + total_rr_by_subclass[str(row["subclass"])] = float( + row["revenue_requirement"] + ) + + subclass_rr: dict[str, dict[str, float]] = {} + for row in delivery_breakdown.to_dicts(): + raw_val = str(row["subclass"]) + alias = gv_map.get(raw_val, raw_val) + delivery = float(row["revenue_requirement"]) + total = total_rr_by_subclass.get(raw_val, delivery) + supply = total - delivery + subclass_rr[alias] = { + "delivery": delivery, + "supply": supply, + "total": total, + } + + all_methods_rr[method_key] = subclass_rr differentiated_data: dict[str, object] = { "utility": utility, "group_col": group_col, - "cross_subsidy_col": cross_subsidy_col, + "allocation_methods": list(all_methods_rr.keys()), "source_run_dir": str(run_dir), } if total_delivery_rr is not None: @@ -795,7 +845,7 @@ def _write_revenue_requirement_yamls( differentiated_data["total_delivery_and_supply_revenue_requirement"] = ( total_delivery_and_supply_rr ) - differentiated_data["subclass_revenue_requirements"] = subclass_rr + differentiated_data["subclass_revenue_requirements"] = all_methods_rr differentiated_yaml_path.write_text( yaml.safe_dump(differentiated_data, sort_keys=False), @@ -843,9 +893,12 @@ def main() -> None: ) parser.add_argument( "--cross-subsidy-col", - default=DEFAULT_BAT_METRIC, - choices=BAT_METRIC_CHOICES, - help="BAT column in cross_subsidization_BAT_values.csv to use.", + default=",".join(SUBCLASS_RR_ALLOCATION_METHODS), + help=( + "Comma-separated BAT columns to compute subclass RR for. " + f"Choices: {', '.join(BAT_METRIC_CHOICES)}. " + f"Default: {','.join(SUBCLASS_RR_ALLOCATION_METHODS)}" + ), ) parser.add_argument( "--annual-month", @@ -956,14 +1009,20 @@ def main() -> None: ) storage_options = get_aws_storage_options() if isinstance(run_dir, S3Path) else None - breakdown = compute_subclass_rr( + cross_subsidy_cols = tuple( + c.strip() for c in args.cross_subsidy_col.split(",") if c.strip() + ) + + delivery_breakdowns = compute_subclass_rr( run_dir=run_dir, group_col=args.group_col, - cross_subsidy_col=args.cross_subsidy_col, + cross_subsidy_cols=cross_subsidy_cols, annual_month=args.annual_month, storage_options=storage_options, ) - print(breakdown) + for col, breakdown in delivery_breakdowns.items(): + print(f"Delivery breakdown ({col}):") + print(breakdown) run_state, run_utility, default_revenue_requirement = _load_run_fields( scenario_config_path=args.scenario_config, @@ -974,7 +1033,6 @@ def main() -> None: if args.group_value_to_subclass: gv_map = parse_group_value_to_subclass(args.group_value_to_subclass) - # Read top-level totals from base RR YAML if provided. total_delivery_rr: float | None = None total_delivery_and_supply_rr: float | None = None if args.base_rr_yaml: @@ -985,7 +1043,7 @@ def main() -> None: base_rr_data["total_delivery_and_supply_revenue_requirement"] ) - total_breakdown: pl.DataFrame | None = None + total_breakdowns: dict[str, pl.DataFrame] | None = None if args.run_dir_supply: run_dir_supply: S3Path | Path = ( S3Path(args.run_dir_supply) @@ -995,27 +1053,27 @@ def main() -> None: storage_options_supply = ( get_aws_storage_options() if isinstance(run_dir_supply, S3Path) else None ) - total_breakdown = compute_subclass_rr( + total_breakdowns = compute_subclass_rr( run_dir=run_dir_supply, group_col=args.group_col, - cross_subsidy_col=args.cross_subsidy_col, + cross_subsidy_cols=cross_subsidy_cols, annual_month=args.annual_month, storage_options=storage_options_supply, ) - LOGGER.info("Run-2 (delivery+supply) breakdown:\n%s", total_breakdown) + for col, tb in total_breakdowns.items(): + LOGGER.info("Run-2 (delivery+supply) breakdown (%s):\n%s", col, tb) if args.write_revenue_requirement_yamls: differentiated_yaml_path, default_yaml_path = _write_revenue_requirement_yamls( - delivery_breakdown=breakdown, + delivery_breakdowns=delivery_breakdowns, run_dir=run_dir, group_col=args.group_col, - cross_subsidy_col=args.cross_subsidy_col, utility=run_utility, default_revenue_requirement=default_revenue_requirement, differentiated_yaml_path=args.differentiated_yaml_path, default_yaml_path=args.default_yaml_path, group_value_to_subclass=gv_map, - total_breakdown=total_breakdown, + total_breakdowns=total_breakdowns, total_delivery_rr=total_delivery_rr, total_delivery_and_supply_rr=total_delivery_and_supply_rr, ) diff --git a/utils/mid/patches.py b/utils/mid/patches.py index bab0e2b9..36282a56 100644 --- a/utils/mid/patches.py +++ b/utils/mid/patches.py @@ -12,11 +12,13 @@ import logging import resource import time +from functools import reduce from pathlib import Path from typing import Any, cast import cairo.rates_tool.loads as _cairo_loads import cairo.rates_tool.lookups as _cairo_sim_lookups +import cairo.rates_tool.postprocessing as _cairo_postproc import cairo.rates_tool.system_revenues as _cairo_sysrev import cairo.rates_tool.systemsimulator as _cairo_sim import numpy as np @@ -1008,3 +1010,288 @@ def _patched_process_residential_hourly_demand( "PATCH_APPLIED cairo.rates_tool.loads.process_residential_hourly_demand -> %s", _patched_process_residential_hourly_demand.__name__, ) + +# --------------------------------------------------------------------------- +# Phase 6: EPMC residual allocation + disable broken peak +# --------------------------------------------------------------------------- +# Adds equi-proportional marginal cost (EPMC) residual allocation to +# CAIRO's cross-subsidization postprocessor. EPMC allocates the residual +# in proportion to each customer's economic burden: +# R_i = R * (EB_i / sum(EB_j * weight_j)) +# This is equivalent to scaling all MC-based rates by K = TRR / MC_Revenue. +# +# Also disables the broken peak residual allocation to free up compute. + +_postproc_log = logging.getLogger("rates_analysis").getChild("postprocessing") + + +def _allocate_residual_epmc( + self: Any, + building_metadata: pd.DataFrame, + annual_customer_economic_burden: pd.Series, + costs_by_type: pd.Series, +) -> pd.Series | None: + """Allocate residual costs in proportion to economic burden (EPMC). + + R_i = R * (EB_i / sum(EB_j * weight_j)) + + Equivalent to scaling all MC-based rates by a uniform factor + K = TRR / MC_Revenue. Mirrors the structure of CAIRO's existing + _allocate_residual_volumetric and _allocate_residual_percustomer. + """ + eb_weighted = pd.merge( + annual_customer_economic_burden, + building_metadata.set_index("bldg_id")[["weight"]], + left_index=True, + right_index=True, + how="left", + ) + denominator = eb_weighted.prod(axis=1).sum() + if denominator == 0: + return None + epmc_rate = costs_by_type["Residual Costs ($)"] / denominator + share = annual_customer_economic_burden.mul(epmc_rate) + share.name = "customer_level_residual_share_epmc" + return share + + +_orig_determine_residual = _cairo_postproc.InternalCrossSubsidizationProcessor._determine_residual_cost_allocation + + +def _patched_determine_residual_cost_allocation( + self: Any, + building_metadata: pd.DataFrame, + raw_hourly_load: pd.DataFrame, + marginal_system_prices: pd.DataFrame, + costs_by_type: pd.Series, + annual_customer_economic_burden: pd.Series, +) -> pd.Series | pd.DataFrame: + """Patched residual allocation: adds EPMC, skips broken peak.""" + vol = self._allocate_residual_volumetric( + building_metadata, raw_hourly_load, costs_by_type + ) + percust = self._allocate_residual_percustomer(building_metadata, costs_by_type) + epmc = self._allocate_residual_epmc( + building_metadata, annual_customer_economic_burden, costs_by_type + ) + + parts: list[pd.Series | pd.DataFrame] = [vol, percust] + if epmc is not None: + parts.append(epmc) + + return reduce( + lambda left, right: pd.merge( + left, right, left_index=True, right_index=True, how="left" + ), + parts, + ) + + +_orig_return_eb_and_residual = _cairo_postproc.InternalCrossSubsidizationProcessor._return_customer_level_economic_burden_and_residual_share + + +def _patched_return_eb_and_residual( + self: Any, + building_metadata: pd.DataFrame, + raw_hourly_load: pd.DataFrame, + marginal_system_prices: pd.DataFrame, + costs_by_type: pd.Series, +) -> tuple[pd.Series, pd.DataFrame]: + """Patched: computes EB first, then threads it into residual allocation.""" + annual_customer_economic_burden = self._determine_marginal_cost_allocation( + raw_hourly_load, marginal_system_prices + ) + annual_customer_residual_share = self._determine_residual_cost_allocation( + building_metadata, + raw_hourly_load, + marginal_system_prices, + costs_by_type, + annual_customer_economic_burden, + ) + return annual_customer_economic_burden, annual_customer_residual_share + + +_orig_return_cross_sub_metrics = _cairo_postproc.InternalCrossSubsidizationProcessor._return_cross_subsidization_metrics + + +def _patched_return_cross_subsidization_metrics( + self: Any, + building_metadata: pd.DataFrame, + raw_hourly_load: pd.DataFrame, + marginal_system_prices: pd.DataFrame, + costs_by_type: pd.Series, + customer_bills: pd.DataFrame, + year_run: int, +) -> None: + """Patched: adds BAT_epmc computation and balance check.""" + economic_burden, residual_share = ( + self._return_customer_level_economic_burden_and_residual_share( + building_metadata, + raw_hourly_load, + marginal_system_prices, + costs_by_type, + ) + ) + + bat_df = reduce( + lambda left, right: pd.merge( + left, right, left_index=True, right_index=True, how="left" + ), + [ + customer_bills.set_index("bldg_id")[["Annual"]], + economic_burden, + residual_share, + building_metadata.set_index("bldg_id")[["weight"]], + ], + ) + + bat_df["BAT_vol"] = bat_df["Annual"].sub( + bat_df["customer_level_economic_burden"].add( + bat_df["customer_level_residual_share_volumetric"] + ) + ) + if "customer_level_residual_share_peak" in bat_df.columns: + bat_df["BAT_peak"] = bat_df["Annual"].sub( + bat_df["customer_level_economic_burden"].add( + bat_df["customer_level_residual_share_peak"] + ) + ) + bat_df["BAT_percustomer"] = bat_df["Annual"].sub( + bat_df["customer_level_economic_burden"].add( + bat_df["customer_level_residual_share_percustomer"] + ) + ) + if "customer_level_residual_share_epmc" in bat_df.columns: + bat_df["BAT_epmc"] = bat_df["Annual"].sub( + bat_df["customer_level_economic_burden"].add( + bat_df["customer_level_residual_share_epmc"] + ) + ) + + if self.run_type == "precalc": + if np.round(bat_df["BAT_vol"].mul(bat_df["weight"]).sum(), 1) != 0: + _postproc_log.error( + "BAT w/ volumetric residual cost allocation imbalanced!" + ) + if "BAT_peak" in bat_df.columns and ( + np.round(bat_df["BAT_peak"].mul(bat_df["weight"]).sum(), 1) != 0 + ): + _postproc_log.error("BAT w/ peak residual cost allocation imbalanced!") + if np.round(bat_df["BAT_percustomer"].mul(bat_df["weight"]).sum(), 1) != 0: + _postproc_log.error( + "BAT w/ per-customer residual cost allocation imbalanced!" + ) + if "BAT_epmc" in bat_df.columns and ( + np.round(bat_df["BAT_epmc"].mul(bat_df["weight"]).sum(), 1) != 0 + ): + _postproc_log.error("BAT w/ EPMC residual cost allocation imbalanced!") + else: + _postproc_log.info( + "WARNING: in default mode there is currently no mechanism to ensure BAT aligns!" + ) + + bat_df["dollar_year"] = year_run + bat_df.to_csv( + self.save_folder / "cross_subsidization" / "cross_subsidization_BAT_values.csv", + index=True, + ) + + self._return_average_bat_by_segment(building_metadata, bat_df) + + +_orig_calculate_bat_stats = ( + _cairo_postproc.InternalCrossSubsidizationProcessor.calculate_BAT_stats_by_group +) + + +def _patched_calculate_bat_stats_by_group( + self: Any, metadata_df: pd.DataFrame, bat_df: pd.DataFrame, gcn: str +) -> pd.DataFrame: + """Patched: includes BAT_epmc in segment stats.""" + from cairo.rates_tool.postprocessing import ( + _group_summary_bat_formatting, + coeff_var, + quartile_coeff_of_disp, + weighted_avg_by_group, + ) + + bat_df_grouped = ( + bat_df.copy().reset_index().merge(metadata_df[["bldg_id", gcn]], on="bldg_id") + ) + bat_cols = [ + col + for col in ("BAT_vol", "BAT_peak", "BAT_percustomer", "BAT_epmc") + if col in bat_df_grouped.columns + ] + if not bat_cols: + return pd.DataFrame() + + bat_var_aggs = {col: ["std", coeff_var, quartile_coeff_of_disp] for col in bat_cols} + bat_var_df = ( + bat_df_grouped.set_index("bldg_id") + .groupby(gcn) + .agg({**bat_var_aggs, "dollar_year": ["first"]}) + ) + bat_var_df = _group_summary_bat_formatting(bat_var_df, gcn) + + bat_stats_aggs = {col: ["mean", "median"] for col in bat_cols} + bat_stats_df = ( + bat_df_grouped.set_index("bldg_id") + .groupby(gcn) + .agg({**bat_stats_aggs, "dollar_year": ["first"]}) + ) + bat_stats_df = _group_summary_bat_formatting(bat_stats_df, gcn) + + bat_weight_aggs = {col: [weighted_avg_by_group] for col in bat_cols} + bat_stats_w_df = ( + bat_df_grouped.set_index("weight") + .groupby(gcn) + .agg({**bat_weight_aggs, "dollar_year": ["first"]}) + ) + bat_stats_w_df = _group_summary_bat_formatting(bat_stats_w_df, gcn) + + bat_df_grouped = reduce( + lambda left, right: pd.merge( + left, right, left_index=True, right_index=True, how="left" + ), + [bat_stats_df, bat_stats_w_df, bat_var_df], + ).reset_index() + + return bat_df_grouped + + +# --- Apply EPMC patches --- +setattr( + _cairo_postproc.InternalCrossSubsidizationProcessor, + "_allocate_residual_epmc", + _allocate_residual_epmc, +) +log.info("PATCH_APPLIED InternalCrossSubsidizationProcessor._allocate_residual_epmc") + +_cairo_postproc.InternalCrossSubsidizationProcessor._determine_residual_cost_allocation = cast( + Any, _patched_determine_residual_cost_allocation +) +log.info( + "PATCH_APPLIED InternalCrossSubsidizationProcessor._determine_residual_cost_allocation" +) + +_cairo_postproc.InternalCrossSubsidizationProcessor._return_customer_level_economic_burden_and_residual_share = cast( + Any, _patched_return_eb_and_residual +) +log.info( + "PATCH_APPLIED InternalCrossSubsidizationProcessor._return_customer_level_economic_burden_and_residual_share" +) + +_cairo_postproc.InternalCrossSubsidizationProcessor._return_cross_subsidization_metrics = cast( + Any, _patched_return_cross_subsidization_metrics +) +log.info( + "PATCH_APPLIED InternalCrossSubsidizationProcessor._return_cross_subsidization_metrics" +) + +_cairo_postproc.InternalCrossSubsidizationProcessor.calculate_BAT_stats_by_group = cast( + Any, _patched_calculate_bat_stats_by_group +) +log.info( + "PATCH_APPLIED InternalCrossSubsidizationProcessor.calculate_BAT_stats_by_group" +) diff --git a/utils/post/build_master_bat.py b/utils/post/build_master_bat.py index 94bfefdc..7e41ceb4 100644 --- a/utils/post/build_master_bat.py +++ b/utils/post/build_master_bat.py @@ -28,17 +28,17 @@ postprocess_group.has_hp, postprocess_group.heating_type, heats_with_electricity, heats_with_natgas, heats_with_oil, heats_with_propane, weight, - BAT_vol_delivery, BAT_vol_supply, BAT_vol_total, - BAT_peak_delivery, BAT_peak_supply, BAT_peak_total, - BAT_percustomer_delivery, BAT_percustomer_supply, BAT_percustomer_total, - annual_bill_delivery, annual_bill_supply, annual_bill_total, - economic_burden_delivery, economic_burden_supply, economic_burden_total, - residual_share_delivery, residual_share_supply, residual_share_total - -Identities (per metric m in {vol, peak, percustomer}): + BAT_{m}_{delivery,supply,total} for each BAT metric present in CAIRO output, + {component}_{delivery,supply,total} for each cost component present. + +Known BAT metrics: BAT_vol, BAT_peak, BAT_percustomer, BAT_epmc. +Known cost components: annual_bill, economic_burden, residual_share, residual_share_epmc. +Metrics not present in the CAIRO CSV are logged and omitted from the output. + +Identities (per metric m in whichever metrics are present): BAT_m_total = BAT_m_delivery + BAT_m_supply -Identities (per component c in {annual_bill, economic_burden, residual_share}): +Identities (per component c): c_total = c_delivery + c_supply annual_bill_total ≈ economic_burden_total + residual_share_total + BAT_percustomer_total """ @@ -62,16 +62,17 @@ UPGRADE_02_RUNS = {3, 4, 7, 8, 11, 12, 19, 20} VALID_RUN_PAIRS = {(r, r + 1) for r in (1, 3, 5, 7, 9, 11, 17, 19)} -BAT_METRICS = ["BAT_vol", "BAT_peak", "BAT_percustomer"] +BAT_METRICS_KNOWN = ["BAT_vol", "BAT_peak", "BAT_percustomer", "BAT_epmc"] # CAIRO source columns → short output names for the cost-allocation components. -# These follow the same delivery/supply/total decomposition as BAT_METRICS. -COST_COMPONENTS_SRC = { +# These follow the same delivery/supply/total decomposition as BAT metrics. +# At runtime, we filter to whichever columns are actually present in the CSV. +COST_COMPONENTS_SRC_KNOWN = { "Annual": "annual_bill", "customer_level_economic_burden": "economic_burden", "customer_level_residual_share_percustomer": "residual_share", + "customer_level_residual_share_epmc": "residual_share_epmc", } -COST_COMPONENTS = list(COST_COMPONENTS_SRC.values()) META_COLS = [ BLDG_ID, @@ -86,7 +87,7 @@ "heats_with_propane", ] -OUTPUT_COLS = [ +META_OUTPUT_COLS = [ BLDG_ID, "sb.electric_utility", "sb.gas_utility", @@ -98,26 +99,19 @@ "heats_with_oil", "heats_with_propane", "weight", - "BAT_vol_delivery", - "BAT_vol_supply", - "BAT_vol_total", - "BAT_peak_delivery", - "BAT_peak_supply", - "BAT_peak_total", - "BAT_percustomer_delivery", - "BAT_percustomer_supply", - "BAT_percustomer_total", - "annual_bill_delivery", - "annual_bill_supply", - "annual_bill_total", - "economic_burden_delivery", - "economic_burden_supply", - "economic_burden_total", - "residual_share_delivery", - "residual_share_supply", - "residual_share_total", ] + +def _build_output_cols(bat_metrics: list[str], cost_components: list[str]) -> list[str]: + """Build the output column list from detected metrics and cost components.""" + cols = list(META_OUTPUT_COLS) + for m in bat_metrics: + cols.extend(f"{m}_{c}" for c in ("delivery", "supply", "total")) + for c in cost_components: + cols.extend(f"{c}_{s}" for s in ("delivery", "supply", "total")) + return cols + + FLOAT_TOL = 1e-4 # --------------------------------------------------------------------------- @@ -390,30 +384,49 @@ def _process_utility( f"{n_weight_diff} rows, max diff={weight_diff.max()}" ) - # --- Validate no nulls in source BAT columns --- - _assert_no_nulls(bat_delivery_df, BAT_METRICS, utility) - _assert_no_nulls(bat_supply_df, BAT_METRICS, utility) + # --- Detect available BAT metrics and cost components --- + delivery_cols = set(bat_delivery_df.columns) + bat_metrics = [m for m in BAT_METRICS_KNOWN if m in delivery_cols] + missing_bat = set(BAT_METRICS_KNOWN) - set(bat_metrics) + if missing_bat: + _log( + f" [{utility}] BAT metrics not in CAIRO output (skipping): " + f"{sorted(missing_bat)}" + ) + + cost_components_src = { + k: v for k, v in COST_COMPONENTS_SRC_KNOWN.items() if k in delivery_cols + } + missing_cost = set(COST_COMPONENTS_SRC_KNOWN) - set(cost_components_src) + if missing_cost: + _log( + f" [{utility}] Cost components not in CAIRO output (skipping): " + f"{sorted(missing_cost)}" + ) + cost_components = list(cost_components_src.values()) - # --- Validate no nulls in cost-component source columns --- - _assert_no_nulls(bat_delivery_df, list(COST_COMPONENTS_SRC.keys()), utility) - _assert_no_nulls(bat_supply_df, list(COST_COMPONENTS_SRC.keys()), utility) + # --- Validate no nulls in source columns --- + _assert_no_nulls(bat_delivery_df, bat_metrics, utility) + _assert_no_nulls(bat_supply_df, bat_metrics, utility) + _assert_no_nulls(bat_delivery_df, list(cost_components_src.keys()), utility) + _assert_no_nulls(bat_supply_df, list(cost_components_src.keys()), utility) # --- Join delivery and supply, compute decomposition --- t = _log(" Computing BAT decomposition (delivery / supply / total)...") delivery_select = ( [BLDG_ID, "weight"] - + [pl.col(m).alias(f"{m}_delivery") for m in BAT_METRICS] + + [pl.col(m).alias(f"{m}_delivery") for m in bat_metrics] + [ pl.col(src).alias(f"{short}_delivery") - for src, short in COST_COMPONENTS_SRC.items() + for src, short in cost_components_src.items() ] ) supply_select = ( [BLDG_ID] - + [pl.col(m).alias(f"{m}_total") for m in BAT_METRICS] + + [pl.col(m).alias(f"{m}_total") for m in bat_metrics] + [ pl.col(src).alias(f"{short}_total") - for src, short in COST_COMPONENTS_SRC.items() + for src, short in cost_components_src.items() ] ) bat = ( @@ -426,11 +439,11 @@ def _process_utility( .with_columns( [ (pl.col(f"{m}_total") - pl.col(f"{m}_delivery")).alias(f"{m}_supply") - for m in BAT_METRICS + for m in bat_metrics ] + [ (pl.col(f"{c}_total") - pl.col(f"{c}_delivery")).alias(f"{c}_supply") - for c in COST_COMPONENTS + for c in cost_components ] ) ) @@ -438,6 +451,7 @@ def _process_utility( # --- Join with metadata --- t = _log(" Joining with metadata...") + output_cols = _build_output_cols(bat_metrics, cost_components) joined = ( bat.join( metadata_for_utility.select(META_COLS), @@ -445,7 +459,7 @@ def _process_utility( how="inner", ) .with_columns(pl.lit(int(upgrade)).alias("upgrade")) - .select(OUTPUT_COLS) + .select(output_cols) ) _log_done(" Joining with metadata", t, f"{joined.height} rows") @@ -455,20 +469,20 @@ def _process_utility( ) # --- Validate identities and nulls --- - for m in BAT_METRICS: + for m in bat_metrics: _assert_bat_identity(joined, m, FLOAT_TOL, utility) - for c in COST_COMPONENTS: + for c in cost_components: _assert_bat_identity(joined, c, FLOAT_TOL, utility) _assert_bill_decomposition(joined, FLOAT_TOL, utility) bat_output_cols = [ f"{m}_{component}" - for m in BAT_METRICS + for m in bat_metrics for component in ("delivery", "supply", "total") ] cost_output_cols = [ f"{c}_{component}" - for c in COST_COMPONENTS + for c in cost_components for component in ("delivery", "supply", "total") ] _assert_no_nulls(joined, ["weight"] + bat_output_cols + cost_output_cols, utility) @@ -618,9 +632,14 @@ def main() -> None: f"Utility {u}: expected {expected} buildings, got {actual}" ) - for m in BAT_METRICS: + master_cols = set(master.columns) + final_bat_metrics = [m for m in BAT_METRICS_KNOWN if f"{m}_delivery" in master_cols] + final_cost_components = [ + v for v in COST_COMPONENTS_SRC_KNOWN.values() if f"{v}_delivery" in master_cols + ] + for m in final_bat_metrics: _assert_bat_identity(master, m, FLOAT_TOL, "ALL") - for c in COST_COMPONENTS: + for c in final_cost_components: _assert_bat_identity(master, c, FLOAT_TOL, "ALL") _assert_bill_decomposition(master, FLOAT_TOL, "ALL") _log_done("Validation", t) diff --git a/utils/pre/create_scenario_yamls.py b/utils/pre/create_scenario_yamls.py index 0658ea0f..cf569d66 100644 --- a/utils/pre/create_scenario_yamls.py +++ b/utils/pre/create_scenario_yamls.py @@ -330,7 +330,6 @@ def parse_required_float(key: str) -> float: return run - def _cell_to_str(value: object) -> str: """Convert sheet cell to string while preserving numeric zeros.""" if value is None: diff --git a/utils/scenario_config.py b/utils/scenario_config.py index 7f62cd60..67cdba82 100644 --- a/utils/scenario_config.py +++ b/utils/scenario_config.py @@ -160,11 +160,14 @@ class RevenueRequirementConfig: rr_total: scalar for MC decomposition (delivery or delivery+supply). subclass_rr: per-tariff-key RR dict, or None for non-subclass runs. run_includes_subclasses: whether the run uses per-subclass RRs. + residual_allocation: the allocation method used (e.g. "percustomer", "epmc"), + or None for non-subclass runs. """ rr_total: float subclass_rr: dict[str, float] | None run_includes_subclasses: bool + residual_allocation: str | None def _parse_subclass_revenue_requirement( @@ -173,17 +176,43 @@ def _parse_subclass_revenue_requirement( base_dir: Path, *, add_supply: bool, + residual_allocation: str, ) -> dict[str, float]: """Map subclass revenue requirements to tariff keys. - YAML subclass keys are aliases ('hp'/'non-hp') matching the keys in - path_tariffs_electric. Each alias resolves to a tariff key (file stem). - Picks 'delivery' or 'total' per subclass based on *add_supply*. + Expects nested YAML structure:: + + subclass_revenue_requirements: + percustomer: + hp: {delivery: ..., supply: ..., total: ...} + non-hp: {delivery: ..., supply: ..., total: ...} + epmc: + hp: {delivery: ..., supply: ..., total: ...} + non-hp: {delivery: ..., supply: ..., total: ...} + + *residual_allocation* selects which allocation-method block to use + (e.g. ``"percustomer"`` or ``"epmc"``). Subclass alias keys + (``"hp"``/``"non-hp"``) match keys in *raw_path_tariffs_electric*. + Picks ``"delivery"`` or ``"total"`` per subclass based on *add_supply*. """ - subclass_rr = rr_data.get("subclass_revenue_requirements") - if not isinstance(subclass_rr, dict) or not subclass_rr: + raw_subclass = rr_data.get("subclass_revenue_requirements") + if not isinstance(raw_subclass, dict) or not raw_subclass: raise ValueError("subclass_revenue_requirements must be a non-empty mapping") + if residual_allocation not in raw_subclass: + available = sorted(raw_subclass.keys()) + raise ValueError( + f"residual_allocation={residual_allocation!r} not found in " + f"subclass_revenue_requirements. Available: {available}" + ) + + subclass_rr = raw_subclass[residual_allocation] + if not isinstance(subclass_rr, dict) or not subclass_rr: + raise ValueError( + f"subclass_revenue_requirements[{residual_allocation!r}] " + "must be a non-empty mapping of subclass aliases" + ) + alias_to_tariff_key = { alias: _resolve_path(str(path_str), base_dir).stem for alias, path_str in raw_path_tariffs_electric.items() @@ -198,21 +227,23 @@ def _parse_subclass_revenue_requirement( f"Subclass alias {alias_str!r} in YAML not found in " f"path_tariffs_electric (available: {sorted(alias_to_tariff_key)})" ) - if isinstance(amount, dict): - rr_field = "total" if add_supply else "delivery" - if rr_field not in amount: - raise ValueError( - f"subclass_revenue_requirements[{alias_str}] missing " - f"required field {rr_field!r}" - ) - result[tariff_key] = _parse_float( - amount[rr_field], - f"subclass_revenue_requirements[{alias_str}].{rr_field}", + if not isinstance(amount, dict): + raise ValueError( + f"subclass_revenue_requirements[{residual_allocation!r}]" + f"[{alias_str}] must be a dict with delivery/supply/total, " + f"got {type(amount).__name__}" ) - else: - result[tariff_key] = _parse_float( - amount, f"subclass_revenue_requirements[{alias_str}]" + rr_field = "total" if add_supply else "delivery" + if rr_field not in amount: + raise ValueError( + f"subclass_revenue_requirements[{residual_allocation!r}]" + f"[{alias_str}] missing required field {rr_field!r}" ) + result[tariff_key] = _parse_float( + amount[rr_field], + f"subclass_revenue_requirements[{residual_allocation!r}]" + f"[{alias_str}].{rr_field}", + ) return result @@ -224,6 +255,7 @@ def _parse_utility_revenue_requirement( *, add_supply: bool, run_includes_subclasses: bool = False, + residual_allocation: str | None = None, ) -> RevenueRequirementConfig: """Parse utility_revenue_requirement from a YAML path. @@ -231,6 +263,10 @@ def _parse_utility_revenue_requirement( - rr_total: scalar from total_delivery[_and_supply]_revenue_requirement - subclass_rr: per-tariff-key RR dict (or None) - run_includes_subclasses: whether this run uses subclass RRs + - residual_allocation: which allocation method block was used + + When *run_includes_subclasses* is True, *residual_allocation* is required + and selects the block within subclass_revenue_requirements to use. """ if not isinstance(value, str): raise ValueError( @@ -262,8 +298,6 @@ def _parse_utility_revenue_requirement( ) if rr_key not in rr_data: - # Fallback: legacy YAMLs (e.g. *_large_number.yaml) use a bare - # 'revenue_requirement' key with the same value for both modes. if "revenue_requirement" in rr_data: rr_total = _parse_float( rr_data["revenue_requirement"], "revenue_requirement" @@ -277,19 +311,29 @@ def _parse_utility_revenue_requirement( subclass_rr: dict[str, float] | None = None if run_includes_subclasses: + if residual_allocation is None: + raise ValueError( + "run_includes_subclasses is true but residual_allocation is not set. " + "Add 'residual_allocation: percustomer' (or 'epmc') to the scenario YAML." + ) if "subclass_revenue_requirements" not in rr_data: raise ValueError( f"run_includes_subclasses is true but {path} has no " "'subclass_revenue_requirements'." ) subclass_rr = _parse_subclass_revenue_requirement( - rr_data, raw_path_tariffs_electric, base_dir, add_supply=add_supply + rr_data, + raw_path_tariffs_electric, + base_dir, + add_supply=add_supply, + residual_allocation=residual_allocation, ) return RevenueRequirementConfig( rr_total=rr_total, subclass_rr=subclass_rr, run_includes_subclasses=run_includes_subclasses, + residual_allocation=residual_allocation, ) From cccd1d4c9894e6e261363562703846d58a11771d Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Thu, 26 Mar 2026 17:39:57 -0400 Subject: [PATCH 02/40] Read run_includes_subclasses and residual_allocation from sheet - create_scenario_yamls.py: read run_includes_subclasses directly from the Google Sheet column instead of re-deriving from tariff key count. Add residual_allocation column handling (optional-if-non-empty pattern). - Tests updated: fixture adds run_includes_subclasses column, derivation test replaced with sheet-read test, new test for residual_allocation. Made-with: Cursor --- tests/test_create_scenario_yamls.py | 30 +++++++++++++++++++++++------ utils/pre/create_scenario_yamls.py | 10 ++++++---- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/tests/test_create_scenario_yamls.py b/tests/test_create_scenario_yamls.py index 72a285e7..76caf1e5 100644 --- a/tests/test_create_scenario_yamls.py +++ b/tests/test_create_scenario_yamls.py @@ -21,6 +21,7 @@ def _base_row() -> dict[str, str]: "path_tariffs_electric": "hp: tariffs/electric/rie_hp_seasonalTOU_supply.json, non-hp: tariffs/electric/rie_flat_supply.json", "utility_revenue_requirement": "rev_requirement/rie_hp_vs_nonhp.yaml", "add_supply_revenue_requirement": "FALSE", + "run_includes_subclasses": "TRUE", "path_electric_utility_stats": "s3://example/eia861.parquet", "solar_pv_compensation": "net_metering", "year_run": "2025", @@ -41,17 +42,34 @@ def test_row_to_run_uses_run_includes_supply() -> None: assert "add_supply_revenue_requirement" not in run -def test_row_to_run_derives_run_includes_subclasses() -> None: - """run_includes_subclasses is derived from path_tariffs_electric keys.""" +def test_row_to_run_reads_run_includes_subclasses_from_sheet() -> None: + """run_includes_subclasses is read from the sheet column, not derived.""" row = _base_row() headers = list(row.keys()) run = _row_to_run(row, headers) assert run["run_includes_subclasses"] is True - row_single = _base_row() - row_single["path_tariffs_electric"] = "all: tariffs/electric/rie_flat.json" - run_single = _row_to_run(row_single, list(row_single.keys())) - assert run_single["run_includes_subclasses"] is False + row_false = _base_row() + row_false["run_includes_subclasses"] = "FALSE" + run_false = _row_to_run(row_false, list(row_false.keys())) + assert run_false["run_includes_subclasses"] is False + + +def test_row_to_run_reads_residual_allocation() -> None: + """residual_allocation is included when non-blank, omitted when blank.""" + row = _base_row() + row["residual_allocation"] = "percustomer" + run = _row_to_run(row, list(row.keys())) + assert run["residual_allocation"] == "percustomer" + + row_blank = _base_row() + row_blank["residual_allocation"] = "" + run_blank = _row_to_run(row_blank, list(row_blank.keys())) + assert "residual_allocation" not in run_blank + + row_absent = _base_row() + run_absent = _row_to_run(row_absent, list(row_absent.keys())) + assert "residual_allocation" not in run_absent def test_row_to_run_includes_path_tou_supply_mc_when_populated() -> None: diff --git a/utils/pre/create_scenario_yamls.py b/utils/pre/create_scenario_yamls.py index cf569d66..6d93c473 100644 --- a/utils/pre/create_scenario_yamls.py +++ b/utils/pre/create_scenario_yamls.py @@ -293,12 +293,14 @@ def parse_required_float(key: str) -> float: supply_raw = require_non_empty("add_supply_revenue_requirement") run["run_includes_supply"] = _parse_bool(supply_raw) - # Derive run_includes_subclasses from path_tariffs_electric keys - tariffs_dict = run.get("path_tariffs_electric") - run["run_includes_subclasses"] = ( - isinstance(tariffs_dict, dict) and len(tariffs_dict) > 1 + run["run_includes_subclasses"] = _parse_bool( + require_non_empty("run_includes_subclasses") ) + residual_allocation = get_optional("residual_allocation") + if residual_allocation: + run["residual_allocation"] = residual_allocation + run["path_electric_utility_stats"] = get("path_electric_utility_stats") path_tou_supply_mc = get_optional("path_tou_supply_mc") From 35cbb420232ee72f4bc47b4ff05fb698f8898535 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Thu, 26 Mar 2026 20:21:35 -0400 Subject: [PATCH 03/40] Comment out peak residual allocation instead of silently omitting Makes the disabled peak allocator visible in the code for future reference. Made-with: Cursor --- utils/mid/patches.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/utils/mid/patches.py b/utils/mid/patches.py index 36282a56..539936b4 100644 --- a/utils/mid/patches.py +++ b/utils/mid/patches.py @@ -1066,16 +1066,21 @@ def _patched_determine_residual_cost_allocation( costs_by_type: pd.Series, annual_customer_economic_burden: pd.Series, ) -> pd.Series | pd.DataFrame: - """Patched residual allocation: adds EPMC, skips broken peak.""" + """Patched residual allocation: adds EPMC, disables broken peak.""" vol = self._allocate_residual_volumetric( building_metadata, raw_hourly_load, costs_by_type ) + # peak = self._allocate_residual_peak( + # building_metadata, raw_hourly_load, marginal_system_prices, costs_by_type + # ) percust = self._allocate_residual_percustomer(building_metadata, costs_by_type) epmc = self._allocate_residual_epmc( building_metadata, annual_customer_economic_burden, costs_by_type ) parts: list[pd.Series | pd.DataFrame] = [vol, percust] + # if peak is not None: + # parts.append(peak) if epmc is not None: parts.append(epmc) From 47ecc279783d8cbfeae9b0cd495fbbf1138a5209 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Thu, 26 Mar 2026 21:17:01 -0400 Subject: [PATCH 04/40] Fix compute-rev-requirements to compute both BAT_percustomer and BAT_epmc The Justfile recipe and its callers explicitly passed "BAT_percustomer", overriding the Python function's default. This caused the RR YAML to only contain the percustomer block, making run 17 crash with "epmc not found". Made-with: Cursor --- rate_design/hp_rates/Justfile | 4 ++-- utils/Justfile | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/rate_design/hp_rates/Justfile b/rate_design/hp_rates/Justfile index 30e409a3..0dba9b87 100644 --- a/rate_design/hp_rates/Justfile +++ b/rate_design/hp_rates/Justfile @@ -363,7 +363,7 @@ create-dist-and-sub-tx-mc-data-all: # MID-CONFIG: generate between runs (using outputs from earlier runs) # ============================================================================= -compute-subclass-rr run_dir scenario_config run_num="1" group_col="has_hp" cross_subsidy_col="BAT_percustomer" differentiated_yaml=path_differentiated_rev_requirement default_yaml=path_default_rev_requirement group_value_to_subclass="true=hp,false=non-hp" base_rr_yaml=path_default_rev_requirement run_dir_supply="" resstock_base="" upgrade="00": +compute-subclass-rr run_dir scenario_config run_num="1" group_col="has_hp" cross_subsidy_col="BAT_percustomer,BAT_epmc" differentiated_yaml=path_differentiated_rev_requirement default_yaml=path_default_rev_requirement group_value_to_subclass="true=hp,false=non-hp" base_rr_yaml=path_default_rev_requirement run_dir_supply="" resstock_base="" upgrade="00": just -f {{ path_repo }}/utils/Justfile compute-subclass-rr \ "{{ run_dir }}" \ "{{ scenario_config }}" \ @@ -427,7 +427,7 @@ compute-rev-requirements: echo ">> compute-rev-requirements: run-1 (delivery) → ${run1_dir}" >&2 echo ">> compute-rev-requirements: run-2 (delivery+supply) → ${run2_dir}" >&2 just compute-subclass-rr "${run1_dir}" "{{ path_scenario_config }}" \ - "1" "has_hp" "BAT_percustomer" \ + "1" "has_hp" "BAT_percustomer,BAT_epmc" \ "{{ path_differentiated_rev_requirement }}" \ "{{ path_default_rev_requirement }}" \ "true=hp,false=non-hp" \ diff --git a/utils/Justfile b/utils/Justfile index 266084a8..7ffc87d3 100644 --- a/utils/Justfile +++ b/utils/Justfile @@ -93,7 +93,7 @@ sweep-tou-window path_supply_energy_mc path_supply_capacity_mc state utility yea # ============================================================================= # Revenue requirement YAML outputs from CAIRO BAT results. -compute-subclass-rr run_dir scenario_config differentiated_yaml_path default_yaml_path run_num="1" group_col="has_hp" cross_subsidy_col="BAT_percustomer" group_value_to_subclass="" base_rr_yaml="" run_dir_supply="" resstock_base="" upgrade="00": +compute-subclass-rr run_dir scenario_config differentiated_yaml_path default_yaml_path run_num="1" group_col="has_hp" cross_subsidy_col="BAT_percustomer,BAT_epmc" group_value_to_subclass="" base_rr_yaml="" run_dir_supply="" resstock_base="" upgrade="00": uv run python {{ project_root }}/utils/mid/compute_subclass_rr.py \ --run-dir "{{ run_dir }}" \ --run-num "{{ run_num }}" \ From f18e064ecef03169782369f8ad94a6eb451b2877 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Thu, 26 Mar 2026 21:18:58 -0400 Subject: [PATCH 05/40] Remove cross_subsidy_col from compute-subclass-rr Justfile recipes The Python CLI default (BAT_percustomer,BAT_epmc) always computes both allocation methods. No reason for the Justfile layer to pass or override it. Made-with: Cursor --- rate_design/hp_rates/Justfile | 5 ++--- utils/Justfile | 3 +-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/rate_design/hp_rates/Justfile b/rate_design/hp_rates/Justfile index 0dba9b87..f8fac5f3 100644 --- a/rate_design/hp_rates/Justfile +++ b/rate_design/hp_rates/Justfile @@ -363,7 +363,7 @@ create-dist-and-sub-tx-mc-data-all: # MID-CONFIG: generate between runs (using outputs from earlier runs) # ============================================================================= -compute-subclass-rr run_dir scenario_config run_num="1" group_col="has_hp" cross_subsidy_col="BAT_percustomer,BAT_epmc" differentiated_yaml=path_differentiated_rev_requirement default_yaml=path_default_rev_requirement group_value_to_subclass="true=hp,false=non-hp" base_rr_yaml=path_default_rev_requirement run_dir_supply="" resstock_base="" upgrade="00": +compute-subclass-rr run_dir scenario_config run_num="1" group_col="has_hp" differentiated_yaml=path_differentiated_rev_requirement default_yaml=path_default_rev_requirement group_value_to_subclass="true=hp,false=non-hp" base_rr_yaml=path_default_rev_requirement run_dir_supply="" resstock_base="" upgrade="00": just -f {{ path_repo }}/utils/Justfile compute-subclass-rr \ "{{ run_dir }}" \ "{{ scenario_config }}" \ @@ -371,7 +371,6 @@ compute-subclass-rr run_dir scenario_config run_num="1" group_col="has_hp" cross "{{ default_yaml }}" \ "{{ run_num }}" \ "{{ group_col }}" \ - "{{ cross_subsidy_col }}" \ "{{ group_value_to_subclass }}" \ "{{ base_rr_yaml }}" \ "{{ run_dir_supply }}" \ @@ -427,7 +426,7 @@ compute-rev-requirements: echo ">> compute-rev-requirements: run-1 (delivery) → ${run1_dir}" >&2 echo ">> compute-rev-requirements: run-2 (delivery+supply) → ${run2_dir}" >&2 just compute-subclass-rr "${run1_dir}" "{{ path_scenario_config }}" \ - "1" "has_hp" "BAT_percustomer,BAT_epmc" \ + "1" "has_hp" \ "{{ path_differentiated_rev_requirement }}" \ "{{ path_default_rev_requirement }}" \ "true=hp,false=non-hp" \ diff --git a/utils/Justfile b/utils/Justfile index 7ffc87d3..aed2b653 100644 --- a/utils/Justfile +++ b/utils/Justfile @@ -93,13 +93,12 @@ sweep-tou-window path_supply_energy_mc path_supply_capacity_mc state utility yea # ============================================================================= # Revenue requirement YAML outputs from CAIRO BAT results. -compute-subclass-rr run_dir scenario_config differentiated_yaml_path default_yaml_path run_num="1" group_col="has_hp" cross_subsidy_col="BAT_percustomer,BAT_epmc" group_value_to_subclass="" base_rr_yaml="" run_dir_supply="" resstock_base="" upgrade="00": +compute-subclass-rr run_dir scenario_config differentiated_yaml_path default_yaml_path run_num="1" group_col="has_hp" group_value_to_subclass="" base_rr_yaml="" run_dir_supply="" resstock_base="" upgrade="00": uv run python {{ project_root }}/utils/mid/compute_subclass_rr.py \ --run-dir "{{ run_dir }}" \ --run-num "{{ run_num }}" \ --scenario-config "{{ scenario_config }}" \ --group-col "{{ group_col }}" \ - --cross-subsidy-col "{{ cross_subsidy_col }}" \ --differentiated-yaml-path "{{ differentiated_yaml_path }}" \ --default-yaml-path "{{ default_yaml_path }}" \ {{ if group_value_to_subclass != "" { "--group-value-to-subclass \"" + group_value_to_subclass + "\"" } else { "" } }} \ From e76dad78ff8d859cdaf7371ecd9fa9b146fba025 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 01:29:07 +0000 Subject: [PATCH 06/40] Fix polars tmp dir bug --- rate_design/hp_rates/ny/state.env | 2 ++ rate_design/hp_rates/ri/state.env | 2 ++ 2 files changed, 4 insertions(+) diff --git a/rate_design/hp_rates/ny/state.env b/rate_design/hp_rates/ny/state.env index 15894436..cf8d34db 100644 --- a/rate_design/hp_rates/ny/state.env +++ b/rate_design/hp_rates/ny/state.env @@ -15,3 +15,5 @@ UTILITIES=or,cenhud,rge,nyseg,psegli,nimo,coned USE_RESSTOCK_LOADS=true # Runs 1-4 use the actual utility rate structure instead of a single flattened volumetric rate. BASE_TARIFF_PATTERN=default +# Polars temp dir under $HOME avoids shared /ebs/tmp ownership conflicts. +POLARS_TEMP_DIR=$HOME/.polars_tmp diff --git a/rate_design/hp_rates/ri/state.env b/rate_design/hp_rates/ri/state.env index 03b7bc88..34a05286 100644 --- a/rate_design/hp_rates/ri/state.env +++ b/rate_design/hp_rates/ri/state.env @@ -16,3 +16,5 @@ USE_RESSTOCK_LOADS=true # Runs 1-4 use the actual utility rate structure (seasonal quarterly flat # for RIE) instead of a single flattened volumetric rate. BASE_TARIFF_PATTERN=default +# Polars temp dir under $HOME avoids shared /ebs/tmp ownership conflicts. +POLARS_TEMP_DIR=$HOME/.polars_tmp From 1b2b02300ce25b4f307a72e4bcdf00a632866a04 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 01:33:42 +0000 Subject: [PATCH 07/40] New rie_hp_flat tariffs --- .../hp_rates/ri/config/tariffs/electric/rie_hp_flat.json | 2 +- .../ri/config/tariffs/electric/rie_hp_flat_calibrated.json | 2 +- .../hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json | 2 +- .../config/tariffs/electric/rie_hp_flat_supply_calibrated.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat.json index fcce082c..01b3bab1 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.0689577481656301, + "rate": 0.08892178419071482, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json index 313ebcca..5a3de9ef 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.06895774816563008, + "rate": 0.08892178419071475, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json index df35118c..0b4dc1dc 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.19101692500754738, + "rate": 0.3013395154610198, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json index df35118c..0b4dc1dc 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.19101692500754738, + "rate": 0.3013395154610198, "adj": 0.0, "unit": "kWh" } From 0e3d92137561b63b7b7a95b0cd4193367e2c487a Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 01:34:19 +0000 Subject: [PATCH 08/40] Brand new rie_hp_nonhp_flat tariffs --- .../tariffs/electric/rie_hp_nonhp_flat.json | 655 ++++++++++++++++++ .../electric/rie_hp_nonhp_flat_supply.json | 655 ++++++++++++++++++ 2 files changed, 1310 insertions(+) create mode 100644 rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json create mode 100644 rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json new file mode 100644 index 00000000..7aed204a --- /dev/null +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json @@ -0,0 +1,655 @@ +{ + "items": [ + { + "label": "rie_hp_nonhp_flat", + "name": "rie_hp_nonhp_flat", + "uri": "", + "sector": "Residential", + "servicetype": "Bundled", + "dgrules": "Net Metering", + "utility": "GenericUtility", + "country": "USA", + "energyweekdayschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyweekendschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyratestructure": [ + [ + { + "rate": 0.0689577481656301, + "adj": 0.0, + "unit": "kWh" + } + ] + ], + "fixedchargefirstmeter": 11.48, + "fixedchargeunits": "$/month", + "mincharge": 0.0, + "minchargeunits": "$/month" + } + ] +} diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json new file mode 100644 index 00000000..79a671b5 --- /dev/null +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json @@ -0,0 +1,655 @@ +{ + "items": [ + { + "label": "rie_hp_nonhp_flat_supply", + "name": "rie_hp_nonhp_flat_supply", + "uri": "", + "sector": "Residential", + "servicetype": "Bundled", + "dgrules": "Net Metering", + "utility": "GenericUtility", + "country": "USA", + "energyweekdayschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyweekendschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyratestructure": [ + [ + { + "rate": 0.19101692500754738, + "adj": 0.0, + "unit": "kWh" + } + ] + ], + "fixedchargefirstmeter": 11.48, + "fixedchargeunits": "$/month", + "mincharge": 0.0, + "minchargeunits": "$/month" + } + ] +} From 3e423dc77555f17ffa916549db3db01d24253d15 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 01:35:32 +0000 Subject: [PATCH 09/40] Slight numerical changes --- .../tariffs/electric/rie_nonhp_default_calibrated.json | 6 +++--- .../electric/rie_nonhp_default_supply_calibrated.json | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_calibrated.json index c5924ab5..c0824e0e 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_calibrated.json @@ -640,21 +640,21 @@ "energyratestructure": [ [ { - "rate": 0.15305741898069447, + "rate": 0.15208952518953966, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.15184267756021277, + "rate": 0.15088246546581316, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.15911594181534697, + "rate": 0.1581097355616256, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply_calibrated.json index bdab2a91..9ca11210 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply_calibrated.json @@ -640,28 +640,28 @@ "energyratestructure": [ [ { - "rate": 0.31990933409290906, + "rate": 0.313832800481621, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.2526131823222592, + "rate": 0.24781490878207596, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.25966109010905025, + "rate": 0.2547289447370959, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.30884487670719407, + "rate": 0.30297850747696603, "adj": 0.0, "unit": "kWh" } From a1fde82cf8c70fa3ec9ef1c85dee1485ef5f2da0 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 01:35:57 +0000 Subject: [PATCH 10/40] New subclass_rr for rie split by residual allocation method --- .../rev_requirement/rie_hp_vs_nonhp.yaml | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml index 10463bdd..0100f9d2 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml @@ -1,17 +1,27 @@ utility: rie group_col: has_hp -source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260324_r1-20_fixedcharges_hpflat/20260324_233238_ri_rie_run1_up00_precalc__default +allocation_methods: +- percustomer +- epmc +source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260326_r1-20_epmc/20260327_005520_ri_rie_run1_up00_precalc__default total_delivery_revenue_requirement: 542556587.55 total_delivery_and_supply_revenue_requirement: 955506957.66 subclass_revenue_requirements: percustomer: non-hp: - delivery: 531018474.4165694 + delivery: 531018474.41656953 supply: 394774670.83571464 - total: 925793145.252284 + total: 925793145.2522842 hp: - delivery: 11538113.133430583 - supply: 18175699.274285078 + delivery: 11538113.13343058 + supply: 18175699.274285085 total: 29713812.407715663 -allocation_methods: - - percustomer + epmc: + non-hp: + delivery: 528045651.5245807 + supply: 381319476.76803267 + total: 909365128.2926134 + hp: + delivery: 14510936.025419151 + supply: 31630893.341966927 + total: 46141829.36738608 From 081b2752fb92ed5a913a3fc6cdee5204e12655e3 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Thu, 26 Mar 2026 21:40:38 -0400 Subject: [PATCH 11/40] Add heating_type_v2 column and informational breakdown in RR YAML - identify_heating_type.py: add postprocess_group.heating_type_v2 with five categories (heat_pump, electrical_resistance, natgas, delivered_fuels, other) alongside the existing heating_type column. - run_scenario.py: include heating_type_v2 in metadata columns so it flows through to CAIRO's customer_metadata.csv. - compute_subclass_rr: compute an informational 5-way breakdown by heating_type_v2 and write it under heating_type_breakdown in the RR YAML. Gracefully skips if the column doesn't exist in metadata. Made-with: Cursor --- data/resstock/identify_heating_type.py | 13 +++++++ rate_design/hp_rates/run_scenario.py | 1 + utils/mid/compute_subclass_rr.py | 53 ++++++++++++++++++++++++++ 3 files changed, 67 insertions(+) diff --git a/data/resstock/identify_heating_type.py b/data/resstock/identify_heating_type.py index df88e780..0f53682e 100644 --- a/data/resstock/identify_heating_type.py +++ b/data/resstock/identify_heating_type.py @@ -122,8 +122,21 @@ def identify_heating_type(metadata: pl.LazyFrame, upgrade_id: str) -> pl.LazyFra | (is_null_up & _col_contains_any(IN_HVAC_COLUMN, PROPANE_SUBSTRINGS)) ).fill_null(False) + heating_type_v2 = ( + pl.when(heating_type_is_hp) + .then(pl.lit("heat_pump")) + .when(heating_type_is_electric_resistance) + .then(pl.lit("electrical_resistance")) + .when(heats_with_natgas) + .then(pl.lit("natgas")) + .when(heats_with_oil | heats_with_propane) + .then(pl.lit("delivered_fuels")) + .otherwise(pl.lit("other")) + ) + return metadata.with_columns( heating_type.alias("postprocess_group.heating_type"), + heating_type_v2.alias("postprocess_group.heating_type_v2"), heats_with_electricity.alias("heats_with_electricity"), heats_with_natgas.alias("heats_with_natgas"), heats_with_oil.alias("heats_with_oil"), diff --git a/rate_design/hp_rates/run_scenario.py b/rate_design/hp_rates/run_scenario.py index eafbcfad..5d1d410a 100644 --- a/rate_design/hp_rates/run_scenario.py +++ b/rate_design/hp_rates/run_scenario.py @@ -564,6 +564,7 @@ def run(settings: ScenarioSettings, num_workers: int | None = None) -> None: "applicability", "postprocess_group.has_hp", "postprocess_group.heating_type", + "postprocess_group.heating_type_v2", "in.vintage_acs", ], ) diff --git a/utils/mid/compute_subclass_rr.py b/utils/mid/compute_subclass_rr.py index 68d3ccec..73f6695d 100644 --- a/utils/mid/compute_subclass_rr.py +++ b/utils/mid/compute_subclass_rr.py @@ -784,6 +784,7 @@ def _write_revenue_requirement_yamls( total_breakdowns: dict[str, pl.DataFrame] | None = None, total_delivery_rr: float | None = None, total_delivery_and_supply_rr: float | None = None, + heating_type_breakdown: dict[str, dict[str, dict[str, float]]] | None = None, ) -> tuple[Path, Path]: """Write per-subclass revenue requirement YAML with nested allocation methods. @@ -846,6 +847,8 @@ def _write_revenue_requirement_yamls( total_delivery_and_supply_rr ) differentiated_data["subclass_revenue_requirements"] = all_methods_rr + if heating_type_breakdown is not None: + differentiated_data["heating_type_breakdown"] = heating_type_breakdown differentiated_yaml_path.write_text( yaml.safe_dump(differentiated_data, sort_keys=False), @@ -1063,6 +1066,55 @@ def main() -> None: for col, tb in total_breakdowns.items(): LOGGER.info("Run-2 (delivery+supply) breakdown (%s):\n%s", col, tb) + # Informational breakdown by heating_type_v2 (if available in metadata). + heating_type_breakdown: dict[str, dict[str, dict[str, float]]] | None = None + try: + ht_delivery = compute_subclass_rr( + run_dir=run_dir, + group_col="heating_type_v2", + cross_subsidy_cols=cross_subsidy_cols, + annual_month=args.annual_month, + storage_options=storage_options, + ) + ht_total: dict[str, pl.DataFrame] | None = None + if args.run_dir_supply: + ht_total = compute_subclass_rr( + run_dir=run_dir_supply, + group_col="heating_type_v2", + cross_subsidy_cols=cross_subsidy_cols, + annual_month=args.annual_month, + storage_options=storage_options_supply, + ) + heating_type_breakdown = {} + for bat_col in cross_subsidy_cols: + method_key = BAT_COL_TO_ALLOCATION_KEY.get(bat_col, bat_col) + ht_del_df = ht_delivery[bat_col] + ht_tot_by_sub: dict[str, float] = {} + if ht_total is not None and bat_col in ht_total: + for row in ht_total[bat_col].to_dicts(): + ht_tot_by_sub[str(row["subclass"])] = float( + row["revenue_requirement"] + ) + method_block: dict[str, dict[str, float]] = {} + for row in ht_del_df.to_dicts(): + sub = str(row["subclass"]) + delivery = float(row["revenue_requirement"]) + total = ht_tot_by_sub.get(sub, delivery) + method_block[sub] = { + "delivery": delivery, + "supply": total - delivery, + "total": total, + } + heating_type_breakdown[method_key] = method_block + for method, block in heating_type_breakdown.items(): + print(f"Heating type breakdown ({method}):") + for sub, vals in block.items(): + print(f" {sub}: {vals}") + except (ValueError, KeyError) as exc: + LOGGER.info( + "Skipping heating_type_v2 breakdown (column may not exist): %s", exc + ) + if args.write_revenue_requirement_yamls: differentiated_yaml_path, default_yaml_path = _write_revenue_requirement_yamls( delivery_breakdowns=delivery_breakdowns, @@ -1076,6 +1128,7 @@ def main() -> None: total_breakdowns=total_breakdowns, total_delivery_rr=total_delivery_rr, total_delivery_and_supply_rr=total_delivery_and_supply_rr, + heating_type_breakdown=heating_type_breakdown, ) print(f"Wrote differentiated YAML: {differentiated_yaml_path}") print(f"Wrote default YAML: {default_yaml_path}") From 8564dac6e98d512e4ce323b2e6b58ec5838fe3ed Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 01:57:21 +0000 Subject: [PATCH 12/40] Add heat_type_breakdown to rie subclass rr yaml --- .../rev_requirement/rie_hp_vs_nonhp.yaml | 59 ++++++++++++++++--- 1 file changed, 51 insertions(+), 8 deletions(-) diff --git a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml index 0100f9d2..f131bda5 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml @@ -3,25 +3,68 @@ group_col: has_hp allocation_methods: - percustomer - epmc -source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260326_r1-20_epmc/20260327_005520_ri_rie_run1_up00_precalc__default +source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260326_r1-20_epmc/20260327_015312_ri_rie_run1_up00_precalc__default total_delivery_revenue_requirement: 542556587.55 total_delivery_and_supply_revenue_requirement: 955506957.66 subclass_revenue_requirements: percustomer: non-hp: delivery: 531018474.41656953 - supply: 394774670.83571464 - total: 925793145.2522842 + supply: 394774670.835715 + total: 925793145.2522845 hp: - delivery: 11538113.13343058 - supply: 18175699.274285085 + delivery: 11538113.133430587 + supply: 18175699.274285078 total: 29713812.407715663 epmc: non-hp: delivery: 528045651.5245807 - supply: 381319476.76803267 - total: 909365128.2926134 + supply: 381319476.7680328 + total: 909365128.2926135 hp: - delivery: 14510936.025419151 + delivery: 14510936.025419153 + supply: 31630893.341966927 + total: 46141829.36738608 +heating_type_breakdown: + percustomer: + delivered_fuels: + delivery: 181332489.1256645 + supply: 135908283.90410492 + total: 317240773.0297694 + electrical_resistance: + delivery: 44312079.81038717 + supply: 66102143.52061749 + total: 110414223.33100466 + heat_pump: + delivery: 11538113.133430578 + supply: 18175699.274285085 + total: 29713812.407715663 + natgas: + delivery: 294621861.52847034 + supply: 184890391.19549322 + total: 479512252.72396356 + other: + delivery: 10752043.952047499 + supply: 7873852.215499504 + total: 18625896.167547002 + epmc: + delivered_fuels: + delivery: 207705127.43377423 + supply: 117706857.03651214 + total: 325411984.47028637 + electrical_resistance: + delivery: 35118464.54736017 + supply: 123390160.52429955 + total: 158508625.0716597 + heat_pump: + delivery: 14510936.025419153 supply: 31630893.341966927 total: 46141829.36738608 + natgas: + delivery: 272906529.7775144 + supply: 133725206.15449703 + total: 406631735.9320114 + other: + delivery: 12315529.765931955 + supply: 6497253.052724594 + total: 18812782.81865655 From 8e47bae5be42bfff613e60512e9952dbd81db12f Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 02:05:26 +0000 Subject: [PATCH 13/40] Renegerate ny scenarios --- .../ny/config/scenarios/scenarios_cenhud.yaml | 12 ++++++------ .../ny/config/scenarios/scenarios_coned.yaml | 12 ++++++------ .../hp_rates/ny/config/scenarios/scenarios_nimo.yaml | 12 ++++++------ .../ny/config/scenarios/scenarios_nyseg.yaml | 12 ++++++------ .../hp_rates/ny/config/scenarios/scenarios_or.yaml | 12 ++++++------ .../ny/config/scenarios/scenarios_psegli.yaml | 12 ++++++------ .../hp_rates/ny/config/scenarios/scenarios_rge.yaml | 12 ++++++------ 7 files changed, 42 insertions(+), 42 deletions(-) diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml index 25a4efdc..7969b8d7 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml @@ -133,6 +133,7 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -140,7 +141,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 6: run_name: ny_cenhud_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,6 +163,7 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -170,7 +171,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 7: run_name: ny_cenhud_run7_up02_default__hp_seasonal state: NY @@ -249,6 +249,7 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -256,7 +257,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 10: run_name: ny_cenhud_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,6 +279,7 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -286,7 +287,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 11: run_name: ny_cenhud_run11_up02_default__hp_seasonalTOU state: NY @@ -365,13 +365,13 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.22 winter: -0.18 @@ -397,13 +397,13 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.22 winter: -0.18 diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml index 4a953f9f..eba150e5 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml @@ -133,6 +133,7 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -140,7 +141,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 6: run_name: ny_coned_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,6 +163,7 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -170,7 +171,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 7: run_name: ny_coned_run7_up02_default__hp_seasonal state: NY @@ -249,6 +249,7 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -256,7 +257,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 10: run_name: ny_coned_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,6 +279,7 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -286,7 +287,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 11: run_name: ny_coned_run11_up02_default__hp_seasonalTOU state: NY @@ -365,13 +365,13 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.18 winter: -0.16 @@ -397,13 +397,13 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.18 winter: -0.16 diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml index 5ddb2327..3dfd78b8 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml @@ -133,6 +133,7 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -140,7 +141,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 6: run_name: ny_nimo_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,6 +163,7 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -170,7 +171,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 7: run_name: ny_nimo_run7_up02_default__hp_seasonal state: NY @@ -249,6 +249,7 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -256,7 +257,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 10: run_name: ny_nimo_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,6 +279,7 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -286,7 +287,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 11: run_name: ny_nimo_run11_up02_default__hp_seasonalTOU state: NY @@ -365,13 +365,13 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.18 winter: -0.16 @@ -397,13 +397,13 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.18 winter: -0.16 diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml index 7e723140..ff3a2ba0 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml @@ -133,6 +133,7 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -140,7 +141,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 6: run_name: ny_nyseg_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,6 +163,7 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -170,7 +171,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 7: run_name: ny_nyseg_run7_up02_default__hp_seasonal state: NY @@ -249,6 +249,7 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -256,7 +257,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 10: run_name: ny_nyseg_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,6 +279,7 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -286,7 +287,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 11: run_name: ny_nyseg_run11_up02_default__hp_seasonalTOU state: NY @@ -365,13 +365,13 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.18 winter: -0.16 @@ -397,13 +397,13 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.18 winter: -0.16 diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml index 67f4e40f..7cb77ec1 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml @@ -133,6 +133,7 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -140,7 +141,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 6: run_name: ny_or_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,6 +163,7 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -170,7 +171,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 7: run_name: ny_or_run7_up02_default__hp_seasonal state: NY @@ -249,6 +249,7 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -256,7 +257,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 10: run_name: ny_or_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,6 +279,7 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -286,7 +287,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 11: run_name: ny_or_run11_up02_default__hp_seasonalTOU state: NY @@ -365,13 +365,13 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.24 winter: -0.2 @@ -397,13 +397,13 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.24 winter: -0.2 diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml index aa081ec8..3f5e6380 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml @@ -133,6 +133,7 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -140,7 +141,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 6: run_name: ny_psegli_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,6 +163,7 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -170,7 +171,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 7: run_name: ny_psegli_run7_up02_default__hp_seasonal state: NY @@ -249,6 +249,7 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -256,7 +257,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 10: run_name: ny_psegli_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,6 +279,7 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -286,7 +287,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 11: run_name: ny_psegli_run11_up02_default__hp_seasonalTOU state: NY @@ -365,13 +365,13 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.22 winter: -0.18 @@ -397,13 +397,13 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.22 winter: -0.18 diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml index 9644a6e1..9e6b4646 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml @@ -133,6 +133,7 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -140,7 +141,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 6: run_name: ny_rge_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,6 +163,7 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -170,7 +171,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 7: run_name: ny_rge_run7_up02_default__hp_seasonal state: NY @@ -249,6 +249,7 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -256,7 +257,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 10: run_name: ny_rge_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,6 +279,7 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -286,7 +287,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation: percustomer 11: run_name: ny_rge_run11_up02_default__hp_seasonalTOU state: NY @@ -365,13 +365,13 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.18 winter: -0.16 @@ -397,13 +397,13 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering year_run: 2025 year_dollar_conversion: 2025 process_workers: 8 - residual_allocation: percustomer elasticity: summer: -0.18 winter: -0.16 From 63df32461cd5f21365f21a711fe92bd31879932f Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 02:06:48 +0000 Subject: [PATCH 14/40] cenhud and rge hp_nonhp_flat --- .../electric/cenhud_hp_nonhp_flat.json | 655 ++++++++++++++++++ .../electric/cenhud_hp_nonhp_flat_supply.json | 655 ++++++++++++++++++ .../tariffs/electric/rge_hp_nonhp_flat.json | 655 ++++++++++++++++++ .../electric/rge_hp_nonhp_flat_supply.json | 655 ++++++++++++++++++ 4 files changed, 2620 insertions(+) create mode 100644 rate_design/hp_rates/ny/config/tariffs/electric/cenhud_hp_nonhp_flat.json create mode 100644 rate_design/hp_rates/ny/config/tariffs/electric/cenhud_hp_nonhp_flat_supply.json create mode 100644 rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_nonhp_flat.json create mode 100644 rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_nonhp_flat_supply.json diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_hp_nonhp_flat.json b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_hp_nonhp_flat.json new file mode 100644 index 00000000..d7496463 --- /dev/null +++ b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_hp_nonhp_flat.json @@ -0,0 +1,655 @@ +{ + "items": [ + { + "label": "cenhud_hp_nonhp_flat", + "name": "cenhud_hp_nonhp_flat", + "uri": "", + "sector": "Residential", + "servicetype": "Bundled", + "dgrules": "Net Metering", + "utility": "GenericUtility", + "country": "USA", + "energyweekdayschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyweekendschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyratestructure": [ + [ + { + "rate": 0.06298683892118259, + "adj": 0.0, + "unit": "kWh" + } + ] + ], + "fixedchargefirstmeter": 22.0, + "fixedchargeunits": "$/month", + "mincharge": 0.0, + "minchargeunits": "$/month" + } + ] +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_hp_nonhp_flat_supply.json b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_hp_nonhp_flat_supply.json new file mode 100644 index 00000000..43cc76da --- /dev/null +++ b/rate_design/hp_rates/ny/config/tariffs/electric/cenhud_hp_nonhp_flat_supply.json @@ -0,0 +1,655 @@ +{ + "items": [ + { + "label": "cenhud_hp_nonhp_flat_supply", + "name": "cenhud_hp_nonhp_flat_supply", + "uri": "", + "sector": "Residential", + "servicetype": "Bundled", + "dgrules": "Net Metering", + "utility": "GenericUtility", + "country": "USA", + "energyweekdayschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyweekendschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyratestructure": [ + [ + { + "rate": 0.14732390252755898, + "adj": 0.0, + "unit": "kWh" + } + ] + ], + "fixedchargefirstmeter": 22.0, + "fixedchargeunits": "$/month", + "mincharge": 0.0, + "minchargeunits": "$/month" + } + ] +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_nonhp_flat.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_nonhp_flat.json new file mode 100644 index 00000000..aec3b5d4 --- /dev/null +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_nonhp_flat.json @@ -0,0 +1,655 @@ +{ + "items": [ + { + "label": "rge_hp_nonhp_flat", + "name": "rge_hp_nonhp_flat", + "uri": "", + "sector": "Residential", + "servicetype": "Bundled", + "dgrules": "Net Metering", + "utility": "GenericUtility", + "country": "USA", + "energyweekdayschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyweekendschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyratestructure": [ + [ + { + "rate": 0.030290745008676276, + "adj": 0.0, + "unit": "kWh" + } + ] + ], + "fixedchargefirstmeter": 23.99, + "fixedchargeunits": "$/month", + "mincharge": 0.0, + "minchargeunits": "$/month" + } + ] +} diff --git a/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_nonhp_flat_supply.json b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_nonhp_flat_supply.json new file mode 100644 index 00000000..d919a448 --- /dev/null +++ b/rate_design/hp_rates/ny/config/tariffs/electric/rge_hp_nonhp_flat_supply.json @@ -0,0 +1,655 @@ +{ + "items": [ + { + "label": "rge_hp_nonhp_flat_supply", + "name": "rge_hp_nonhp_flat_supply", + "uri": "", + "sector": "Residential", + "servicetype": "Bundled", + "dgrules": "Net Metering", + "utility": "GenericUtility", + "country": "USA", + "energyweekdayschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyweekendschedule": [ + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ], + [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0 + ] + ], + "energyratestructure": [ + [ + { + "rate": 0.10369079969547533, + "adj": 0.0, + "unit": "kWh" + } + ] + ], + "fixedchargefirstmeter": 23.99, + "fixedchargeunits": "$/month", + "mincharge": 0.0, + "minchargeunits": "$/month" + } + ] +} From ec0d492275041e2c5c8877575c1046d06cafc69e Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Thu, 26 Mar 2026 22:51:24 -0400 Subject: [PATCH 15/40] Add build-all-master recipe to RI Justfile Runs build-master-bills-with-lmi + build-master-bat for all 10 run pairs (1+2 through 19+20) in one command. Usage: just s ri build-all-master Made-with: Cursor --- rate_design/hp_rates/ri/Justfile | 11 +++++++++++ .../ri/config/rev_requirement/rie_hp_vs_nonhp.yaml | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/rate_design/hp_rates/ri/Justfile b/rate_design/hp_rates/ri/Justfile index 79ea5fc3..72fca691 100644 --- a/rate_design/hp_rates/ri/Justfile +++ b/rate_design/hp_rates/ri/Justfile @@ -53,6 +53,17 @@ build-master-bills-with-lmi batch run_delivery run_supply *script_args: --lmi-seed 42 \ {{ script_args }} +build-all-master batch *script_args: + #!/usr/bin/env bash + set -euo pipefail + for d in 1 3 5 7 9 11 13 15 17 19; do + s=$((d + 1)) + echo ">> master bills+bat for runs ${d}+${s}" >&2 + just build-master-bills-with-lmi "{{ batch }}" "$d" "$s" {{ script_args }} + just build-master-bat "{{ batch }}" "$d" "$s" + done + echo ">> all master bills+bat complete" >&2 + # ============================================================================= # RI-only: bulk transmission marginal cost generation (AESC PTF) # ============================================================================= diff --git a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml index f131bda5..4df6e3be 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml @@ -1,8 +1,8 @@ utility: rie group_col: has_hp allocation_methods: -- percustomer -- epmc + - percustomer + - epmc source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260326_r1-20_epmc/20260327_015312_ri_rie_run1_up00_precalc__default total_delivery_revenue_requirement: 542556587.55 total_delivery_and_supply_revenue_requirement: 955506957.66 From 7f2bb783cfb7af14526b541bd68b21c20f170491 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Thu, 26 Mar 2026 22:54:09 -0400 Subject: [PATCH 16/40] Add build-all-master recipe to NY Justfile Runs build-master-bills-with-lmi + build-master-bat for all 8 NY run pairs (1+2 through 15+16) in one command. Usage: just s ny build-all-master Made-with: Cursor --- rate_design/hp_rates/ny/Justfile | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/rate_design/hp_rates/ny/Justfile b/rate_design/hp_rates/ny/Justfile index 43289c99..1dce4f8e 100644 --- a/rate_design/hp_rates/ny/Justfile +++ b/rate_design/hp_rates/ny/Justfile @@ -200,6 +200,17 @@ build-master-bills-with-lmi-all batch *script_args: just build-master-bills-with-lmi "{{ batch }}" 9 10 {{ script_args }} just build-master-bills-with-lmi "{{ batch }}" 11 12 {{ script_args }} +build-all-master batch *script_args: + #!/usr/bin/env bash + set -euo pipefail + for d in 1 3 5 7 9 11 13 15; do + s=$((d + 1)) + echo ">> master bills+bat for runs ${d}+${s}" >&2 + just build-master-bills-with-lmi "{{ batch }}" "$d" "$s" {{ script_args }} + just build-master-bat "{{ batch }}" "$d" "$s" + done + echo ">> all master bills+bat complete" >&2 + apply-lmi-to-existing-master-bills batch run_delivery run_supply participation_rate="1.0" participation_mode="weighted" seed="42" calculation_type="budget" output_path="": uv run python {{ path_repo }}/utils/post/apply_ny_lmi_to_master_bills.py \ --master-bills-path "s3://data.sb/switchbox/cairo/outputs/hp_rates/{{ state }}/all_utilities/{{ batch }}/run_{{ run_delivery }}+{{ run_supply }}/comb_bills_year_target/" \ From cbf5e22ca61778b1830aee79dd5850b6e9c5fd1b Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Fri, 27 Mar 2026 01:29:39 -0400 Subject: [PATCH 17/40] Separate delivery and supply allocation methods in subclass RR YAML MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The run 1/run 2 subtraction architecture produces incorrect supply RRs when EPMC is used, because EB weights differ between runs (HP = 2.67% in delivery-only run vs 4.83% in delivery+supply). This created a phantom $15.9M "supply EPMC residual" that inflated HP total RR by $16M. Fix: restructure the YAML into independent delivery and supply blocks. Each run picks one delivery method and one supply method independently. Delivery methods: passthrough, percustomer, epmc, volumetric Supply methods: passthrough, percustomer, volumetric (Supply EPMC omitted — broken by subtraction; volumetric gives same answer) Supply passthrough = actual supply bills (no BAT adjustment) = same supply rate as default. This is correct for delivery-only rate designs (seasonal, flat HP) where supply shouldn't change. Supply percustomer/volumetric = BAT-adjusted supply (clean subtraction, weights are constant). Correct for TOU runs that address supply cross-sub. Made-with: Cursor --- .../rev_requirement/cenhud_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/coned_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/nimo_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/nyseg_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/or_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/psegli_hp_vs_nonhp.yaml | 20 +- .../rev_requirement/rge_hp_vs_nonhp.yaml | 20 +- .../ny/config/scenarios/scenarios_cenhud.yaml | 18 +- .../ny/config/scenarios/scenarios_coned.yaml | 18 +- .../ny/config/scenarios/scenarios_nimo.yaml | 18 +- .../ny/config/scenarios/scenarios_nyseg.yaml | 18 +- .../ny/config/scenarios/scenarios_or.yaml | 18 +- .../ny/config/scenarios/scenarios_psegli.yaml | 18 +- .../ny/config/scenarios/scenarios_rge.yaml | 18 +- .../rev_requirement/rie_hp_vs_nonhp.yaml | 33 ++- .../ri/config/scenarios/scenarios_rie.yaml | 24 ++- rate_design/hp_rates/run_scenario.py | 20 +- tests/test_create_scenario_yamls.py | 17 +- tests/test_scenario_config.py | 36 ++-- tests/test_subclass_rr.py | 191 +++++++++--------- utils/mid/compute_subclass_rr.py | 108 ++++++---- utils/post/validate/__main__.py | 30 ++- utils/pre/create_scenario_yamls.py | 9 +- utils/scenario_config.py | 160 ++++++++++----- 24 files changed, 534 insertions(+), 360 deletions(-) diff --git a/rate_design/hp_rates/ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml index f96ca992..e1d9db3b 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml @@ -4,14 +4,12 @@ source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/cenhud/ny_20260 total_delivery_revenue_requirement: 418151641.73 total_delivery_and_supply_revenue_requirement: 646688375.32 subclass_revenue_requirements: - percustomer: - non-hp: - delivery: 401392812.56143004 - supply: 209823391.60389036 - total: 611216204.1653204 - hp: - delivery: 16758829.168569993 - supply: 18713341.9861097 - total: 35472171.15467969 -allocation_methods: - - percustomer + delivery: + percustomer: + non-hp: 401392812.56143004 + hp: 16758829.168569993 + supply: + passthrough: &id001 + non-hp: 209823391.60389036 + hp: 18713341.9861097 + percustomer: *id001 diff --git a/rate_design/hp_rates/ny/config/rev_requirement/coned_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/coned_hp_vs_nonhp.yaml index 3e1df225..d9c42735 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/coned_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/coned_hp_vs_nonhp.yaml @@ -4,14 +4,12 @@ source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/coned/ny_202603 total_delivery_revenue_requirement: 3287782738.4 total_delivery_and_supply_revenue_requirement: 4894675585.19 subclass_revenue_requirements: - percustomer: - non-hp: - delivery: 3208744948.2210865 - supply: 1544462515.047821 - total: 4753207463.268908 - hp: - delivery: 79037790.17891257 - supply: 62430331.74217631 - total: 141468121.92108887 -allocation_methods: - - percustomer + delivery: + percustomer: + non-hp: 3208744948.2210865 + hp: 79037790.17891257 + supply: + passthrough: &id001 + non-hp: 1544462515.047821 + hp: 62430331.74217631 + percustomer: *id001 diff --git a/rate_design/hp_rates/ny/config/rev_requirement/nimo_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/nimo_hp_vs_nonhp.yaml index 771d897c..2b63fbea 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/nimo_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/nimo_hp_vs_nonhp.yaml @@ -4,14 +4,12 @@ source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/nimo/ny_2026032 total_delivery_revenue_requirement: 1419812826.56 total_delivery_and_supply_revenue_requirement: 2572877123.2 subclass_revenue_requirements: - percustomer: - non-hp: - delivery: 1371379962.664701 - supply: 1077043154.9046779 - total: 2448423117.569379 - hp: - delivery: 48432863.89529973 - supply: 76021141.73532248 - total: 124454005.63062221 -allocation_methods: - - percustomer + delivery: + percustomer: + non-hp: 1371379962.664701 + hp: 48432863.89529973 + supply: + passthrough: &id001 + non-hp: 1077043154.9046779 + hp: 76021141.73532248 + percustomer: *id001 diff --git a/rate_design/hp_rates/ny/config/rev_requirement/nyseg_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/nyseg_hp_vs_nonhp.yaml index 601f06cd..5ed7ce62 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/nyseg_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/nyseg_hp_vs_nonhp.yaml @@ -4,14 +4,12 @@ source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/nyseg/ny_202603 total_delivery_revenue_requirement: 913708327.54 total_delivery_and_supply_revenue_requirement: 1617781931.62 subclass_revenue_requirements: - percustomer: - non-hp: - delivery: 882168118.8739239 - supply: 660518330.1133512 - total: 1542686448.9872751 - hp: - delivery: 31540208.666075975 - supply: 43555273.96664974 - total: 75095482.63272572 -allocation_methods: - - percustomer + delivery: + percustomer: + non-hp: 882168118.8739239 + hp: 31540208.666075975 + supply: + passthrough: &id001 + non-hp: 660518330.1133512 + hp: 43555273.96664974 + percustomer: *id001 diff --git a/rate_design/hp_rates/ny/config/rev_requirement/or_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/or_hp_vs_nonhp.yaml index 1955554b..df2ed89d 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/or_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/or_hp_vs_nonhp.yaml @@ -4,14 +4,12 @@ source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/or/ny_20260323a total_delivery_revenue_requirement: 263364257.0 total_delivery_and_supply_revenue_requirement: 431562114.21 subclass_revenue_requirements: - percustomer: - non-hp: - delivery: 254976490.86444885 - supply: 158986971.95407706 - total: 413963462.8185259 - hp: - delivery: 8387766.13555118 - supply: 9210885.255922925 - total: 17598651.391474105 -allocation_methods: - - percustomer + delivery: + percustomer: + non-hp: 254976490.86444885 + hp: 8387766.13555118 + supply: + passthrough: &id001 + non-hp: 158986971.95407706 + hp: 9210885.255922925 + percustomer: *id001 diff --git a/rate_design/hp_rates/ny/config/rev_requirement/psegli_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/psegli_hp_vs_nonhp.yaml index 0eec6d45..1852f230 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/psegli_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/psegli_hp_vs_nonhp.yaml @@ -4,14 +4,12 @@ source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/psegli/ny_20260 total_delivery_revenue_requirement: 1167061663.64 total_delivery_and_supply_revenue_requirement: 2152328483.56 subclass_revenue_requirements: - percustomer: - non-hp: - delivery: 1150589335.0526578 - supply: 959472648.3151505 - total: 2110061983.3678083 - hp: - delivery: 16472328.587342722 - supply: 25794171.604849398 - total: 42266500.19219212 -allocation_methods: - - percustomer + delivery: + percustomer: + non-hp: 1150589335.0526578 + hp: 16472328.587342722 + supply: + passthrough: &id001 + non-hp: 959472648.3151505 + hp: 25794171.604849398 + percustomer: *id001 diff --git a/rate_design/hp_rates/ny/config/rev_requirement/rge_hp_vs_nonhp.yaml b/rate_design/hp_rates/ny/config/rev_requirement/rge_hp_vs_nonhp.yaml index 9920e6fe..4bf4958f 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/rge_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ny/config/rev_requirement/rge_hp_vs_nonhp.yaml @@ -4,14 +4,12 @@ source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/rge/ny_20260323 total_delivery_revenue_requirement: 327173904.04 total_delivery_and_supply_revenue_requirement: 601019871.4 subclass_revenue_requirements: - percustomer: - non-hp: - delivery: 312450301.38605285 - supply: 249035038.35532916 - total: 561485339.741382 - hp: - delivery: 14723602.653947193 - supply: 24810929.004670765 - total: 39534531.65861796 -allocation_methods: - - percustomer + delivery: + percustomer: + non-hp: 312450301.38605285 + hp: 14723602.653947193 + supply: + passthrough: &id001 + non-hp: 249035038.35532916 + hp: 24810929.004670765 + percustomer: *id001 diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml index 7969b8d7..07b27df7 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml @@ -133,7 +133,6 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -141,6 +140,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 6: run_name: ny_cenhud_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,7 +164,6 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,6 +171,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 7: run_name: ny_cenhud_run7_up02_default__hp_seasonal state: NY @@ -249,7 +251,6 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -257,6 +258,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 10: run_name: ny_cenhud_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,7 +282,6 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -287,6 +289,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 11: run_name: ny_cenhud_run11_up02_default__hp_seasonalTOU state: NY @@ -365,7 +369,6 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -375,6 +378,8 @@ runs: elasticity: summer: -0.22 winter: -0.18 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 14: run_name: ny_cenhud_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,7 +402,6 @@ runs: utility_revenue_requirement: rev_requirement/cenhud_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=cenhud/year=2025/data.parquet solar_pv_compensation: net_metering @@ -407,6 +411,8 @@ runs: elasticity: summer: -0.22 winter: -0.18 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 15: run_name: ny_cenhud_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml index eba150e5..d753f9fa 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_coned.yaml @@ -133,7 +133,6 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -141,6 +140,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 6: run_name: ny_coned_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,7 +164,6 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,6 +171,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 7: run_name: ny_coned_run7_up02_default__hp_seasonal state: NY @@ -249,7 +251,6 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -257,6 +258,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 10: run_name: ny_coned_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,7 +282,6 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -287,6 +289,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 11: run_name: ny_coned_run11_up02_default__hp_seasonalTOU state: NY @@ -365,7 +369,6 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -375,6 +378,8 @@ runs: elasticity: summer: -0.18 winter: -0.16 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 14: run_name: ny_coned_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,7 +402,6 @@ runs: utility_revenue_requirement: rev_requirement/coned_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=coned/year=2025/data.parquet solar_pv_compensation: net_metering @@ -407,6 +411,8 @@ runs: elasticity: summer: -0.18 winter: -0.16 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 15: run_name: ny_coned_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml index 3dfd78b8..b33847ca 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_nimo.yaml @@ -133,7 +133,6 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -141,6 +140,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 6: run_name: ny_nimo_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,7 +164,6 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,6 +171,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 7: run_name: ny_nimo_run7_up02_default__hp_seasonal state: NY @@ -249,7 +251,6 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -257,6 +258,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 10: run_name: ny_nimo_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,7 +282,6 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -287,6 +289,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 11: run_name: ny_nimo_run11_up02_default__hp_seasonalTOU state: NY @@ -365,7 +369,6 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -375,6 +378,8 @@ runs: elasticity: summer: -0.18 winter: -0.16 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 14: run_name: ny_nimo_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,7 +402,6 @@ runs: utility_revenue_requirement: rev_requirement/nimo_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nimo/year=2025/data.parquet solar_pv_compensation: net_metering @@ -407,6 +411,8 @@ runs: elasticity: summer: -0.18 winter: -0.16 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 15: run_name: ny_nimo_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml index ff3a2ba0..756a1e2b 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_nyseg.yaml @@ -133,7 +133,6 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -141,6 +140,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 6: run_name: ny_nyseg_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,7 +164,6 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,6 +171,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 7: run_name: ny_nyseg_run7_up02_default__hp_seasonal state: NY @@ -249,7 +251,6 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -257,6 +258,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 10: run_name: ny_nyseg_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,7 +282,6 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -287,6 +289,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 11: run_name: ny_nyseg_run11_up02_default__hp_seasonalTOU state: NY @@ -365,7 +369,6 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -375,6 +378,8 @@ runs: elasticity: summer: -0.18 winter: -0.16 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 14: run_name: ny_nyseg_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,7 +402,6 @@ runs: utility_revenue_requirement: rev_requirement/nyseg_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=nyseg/year=2025/data.parquet solar_pv_compensation: net_metering @@ -407,6 +411,8 @@ runs: elasticity: summer: -0.18 winter: -0.16 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 15: run_name: ny_nyseg_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml index 7cb77ec1..01da7c77 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_or.yaml @@ -133,7 +133,6 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -141,6 +140,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 6: run_name: ny_or_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,7 +164,6 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,6 +171,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 7: run_name: ny_or_run7_up02_default__hp_seasonal state: NY @@ -249,7 +251,6 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -257,6 +258,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 10: run_name: ny_or_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,7 +282,6 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -287,6 +289,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 11: run_name: ny_or_run11_up02_default__hp_seasonalTOU state: NY @@ -365,7 +369,6 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -375,6 +378,8 @@ runs: elasticity: summer: -0.24 winter: -0.2 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 14: run_name: ny_or_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,7 +402,6 @@ runs: utility_revenue_requirement: rev_requirement/or_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=or/year=2025/data.parquet solar_pv_compensation: net_metering @@ -407,6 +411,8 @@ runs: elasticity: summer: -0.24 winter: -0.2 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 15: run_name: ny_or_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml index 3f5e6380..9a36fb7f 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_psegli.yaml @@ -133,7 +133,6 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -141,6 +140,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 6: run_name: ny_psegli_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,7 +164,6 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,6 +171,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 7: run_name: ny_psegli_run7_up02_default__hp_seasonal state: NY @@ -249,7 +251,6 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -257,6 +258,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 10: run_name: ny_psegli_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,7 +282,6 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -287,6 +289,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 11: run_name: ny_psegli_run11_up02_default__hp_seasonalTOU state: NY @@ -365,7 +369,6 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -375,6 +378,8 @@ runs: elasticity: summer: -0.22 winter: -0.18 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 14: run_name: ny_psegli_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,7 +402,6 @@ runs: utility_revenue_requirement: rev_requirement/psegli_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=psegli/year=2025/data.parquet solar_pv_compensation: net_metering @@ -407,6 +411,8 @@ runs: elasticity: summer: -0.22 winter: -0.18 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 15: run_name: ny_psegli_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml b/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml index 9e6b4646..a4b516bd 100644 --- a/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml +++ b/rate_design/hp_rates/ny/config/scenarios/scenarios_rge.yaml @@ -133,7 +133,6 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -141,6 +140,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 6: run_name: ny_rge_run6_up00_precalc_supply__hp_seasonal_vs_default state: NY @@ -163,7 +164,6 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,6 +171,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 7: run_name: ny_rge_run7_up02_default__hp_seasonal state: NY @@ -249,7 +251,6 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -257,6 +258,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 10: run_name: ny_rge_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: NY @@ -279,7 +282,6 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -287,6 +289,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 11: run_name: ny_rge_run11_up02_default__hp_seasonalTOU state: NY @@ -365,7 +369,6 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -375,6 +378,8 @@ runs: elasticity: summer: -0.18 winter: -0.16 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 14: run_name: ny_rge_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: NY @@ -397,7 +402,6 @@ runs: utility_revenue_requirement: rev_requirement/rge_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=NY/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ny/bulk_tx/utility=rge/year=2025/data.parquet solar_pv_compensation: net_metering @@ -407,6 +411,8 @@ runs: elasticity: summer: -0.18 winter: -0.16 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 15: run_name: ny_rge_run15_up02_default__hp_seasonalTOU_flex state: NY diff --git a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml index 4df6e3be..d2477b38 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml @@ -1,30 +1,21 @@ utility: rie group_col: has_hp -allocation_methods: - - percustomer - - epmc source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260326_r1-20_epmc/20260327_015312_ri_rie_run1_up00_precalc__default total_delivery_revenue_requirement: 542556587.55 total_delivery_and_supply_revenue_requirement: 955506957.66 subclass_revenue_requirements: - percustomer: - non-hp: - delivery: 531018474.41656953 - supply: 394774670.835715 - total: 925793145.2522845 - hp: - delivery: 11538113.133430587 - supply: 18175699.274285078 - total: 29713812.407715663 - epmc: - non-hp: - delivery: 528045651.5245807 - supply: 381319476.7680328 - total: 909365128.2926135 - hp: - delivery: 14510936.025419153 - supply: 31630893.341966927 - total: 46141829.36738608 + delivery: + percustomer: + non-hp: 531018474.41656953 + hp: 11538113.133430587 + epmc: + non-hp: 528045651.5245807 + hp: 14510936.025419153 + supply: + passthrough: &id001 + non-hp: 394774670.835715 + hp: 18175699.274285078 + percustomer: *id001 heating_type_breakdown: percustomer: delivered_fuels: diff --git a/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml b/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml index 5aeb9ea1..ae68e154 100644 --- a/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml +++ b/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml @@ -133,7 +133,6 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -141,6 +140,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 6: run_name: ri_rie_run6_up00_precalc_supply__hp_seasonal_vs_default state: RI @@ -163,7 +164,6 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,6 +171,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: passthrough 7: run_name: ri_rie_run7_up02_default__hp_seasonal state: RI @@ -249,7 +251,6 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -257,6 +258,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 10: run_name: ri_rie_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: RI @@ -279,7 +282,6 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -287,6 +289,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 11: run_name: ri_rie_run11_up02_default__hp_seasonalTOU state: RI @@ -365,7 +369,6 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -373,6 +376,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 14: run_name: ri_rie_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: RI @@ -395,7 +400,6 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -403,6 +407,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer 15: run_name: ri_rie_run15_up02_default__hp_seasonalTOU_flex state: RI @@ -481,7 +487,6 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true - residual_allocation: epmc path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -489,6 +494,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: epmc + residual_allocation_supply: passthrough 18: run_name: ri_rie_run18_up00_precalc_supply__hp_flat_vs_default state: RI @@ -511,7 +518,6 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true - residual_allocation: epmc path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -519,6 +525,8 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 + residual_allocation_delivery: epmc + residual_allocation_supply: passthrough 19: run_name: ri_rie_run19_up02_default__hp_flat state: RI diff --git a/rate_design/hp_rates/run_scenario.py b/rate_design/hp_rates/run_scenario.py index 5d1d410a..cc2e9dde 100644 --- a/rate_design/hp_rates/run_scenario.py +++ b/rate_design/hp_rates/run_scenario.py @@ -99,7 +99,8 @@ class ScenarioSettings: rr_total: float subclass_rr: dict[str, float] | None run_includes_subclasses: bool - residual_allocation: str | None + residual_allocation_delivery: str | None + residual_allocation_supply: str | None path_electric_utility_stats: str | Path path_supply_energy_mc: str | Path path_supply_capacity_mc: str | Path @@ -242,17 +243,23 @@ def _build_settings_from_yaml_run( _require_value(run, "run_includes_subclasses"), "run_includes_subclasses", ) - residual_allocation: str | None = run.get("residual_allocation") + residual_allocation_delivery: str | None = run.get("residual_allocation_delivery") + residual_allocation_supply: str | None = run.get("residual_allocation_supply") rr_config: RevenueRequirementConfig = _parse_utility_revenue_requirement( _require_value(run, "utility_revenue_requirement"), path_config, raw_path_tariffs_electric, add_supply=run_includes_supply, run_includes_subclasses=run_includes_subclasses, - residual_allocation=residual_allocation, + residual_allocation_delivery=residual_allocation_delivery, + residual_allocation_supply=residual_allocation_supply, ) - if residual_allocation: - log.info("Residual allocation method: %s", residual_allocation) + if residual_allocation_delivery: + log.info( + "Residual allocation: delivery=%s, supply=%s", + residual_allocation_delivery, + residual_allocation_supply, + ) path_tariff_maps_gas = _resolve_path( str(_require_value(run, "path_tariff_maps_gas")), path_config, @@ -322,7 +329,8 @@ def _build_settings_from_yaml_run( rr_total=rr_config.rr_total, subclass_rr=rr_config.subclass_rr, run_includes_subclasses=rr_config.run_includes_subclasses, - residual_allocation=rr_config.residual_allocation, + residual_allocation_delivery=rr_config.residual_allocation_delivery, + residual_allocation_supply=rr_config.residual_allocation_supply, path_electric_utility_stats=path_electric_utility_stats, year_run=year_run, year_dollar_conversion=year_dollar_conversion, diff --git a/tests/test_create_scenario_yamls.py b/tests/test_create_scenario_yamls.py index 76caf1e5..56484d78 100644 --- a/tests/test_create_scenario_yamls.py +++ b/tests/test_create_scenario_yamls.py @@ -56,20 +56,25 @@ def test_row_to_run_reads_run_includes_subclasses_from_sheet() -> None: def test_row_to_run_reads_residual_allocation() -> None: - """residual_allocation is included when non-blank, omitted when blank.""" + """residual_allocation_delivery/supply are included when non-blank, omitted when blank.""" row = _base_row() - row["residual_allocation"] = "percustomer" + row["residual_allocation_delivery"] = "percustomer" + row["residual_allocation_supply"] = "passthrough" run = _row_to_run(row, list(row.keys())) - assert run["residual_allocation"] == "percustomer" + assert run["residual_allocation_delivery"] == "percustomer" + assert run["residual_allocation_supply"] == "passthrough" row_blank = _base_row() - row_blank["residual_allocation"] = "" + row_blank["residual_allocation_delivery"] = "" + row_blank["residual_allocation_supply"] = "" run_blank = _row_to_run(row_blank, list(row_blank.keys())) - assert "residual_allocation" not in run_blank + assert "residual_allocation_delivery" not in run_blank + assert "residual_allocation_supply" not in run_blank row_absent = _base_row() run_absent = _row_to_run(row_absent, list(row_absent.keys())) - assert "residual_allocation" not in run_absent + assert "residual_allocation_delivery" not in run_absent + assert "residual_allocation_supply" not in run_absent def test_row_to_run_includes_path_tou_supply_mc_when_populated() -> None: diff --git a/tests/test_scenario_config.py b/tests/test_scenario_config.py index c4ca7846..0c10c388 100644 --- a/tests/test_scenario_config.py +++ b/tests/test_scenario_config.py @@ -63,7 +63,8 @@ def test_subclass_delivery_only(self) -> None: }, add_supply=False, run_includes_subclasses=True, - residual_allocation="percustomer", + residual_allocation_delivery="percustomer", + residual_allocation_supply="passthrough", ) assert result.run_includes_subclasses is True assert result.subclass_rr is not None @@ -85,7 +86,8 @@ def test_subclass_delivery_plus_supply(self) -> None: }, add_supply=True, run_includes_subclasses=True, - residual_allocation="percustomer", + residual_allocation_delivery="percustomer", + residual_allocation_supply="passthrough", ) assert result.subclass_rr is not None assert set(result.subclass_rr.keys()) == { @@ -106,7 +108,8 @@ def test_subclass_flex_delivery_only(self) -> None: }, add_supply=False, run_includes_subclasses=True, - residual_allocation="percustomer", + residual_allocation_delivery="percustomer", + residual_allocation_supply="passthrough", ) assert result.subclass_rr is not None assert set(result.subclass_rr.keys()) == { @@ -148,11 +151,12 @@ def test_subclass_run_without_subclass_data_raises(self, tmp_path: Path) -> None }, add_supply=False, run_includes_subclasses=True, - residual_allocation="percustomer", + residual_allocation_delivery="percustomer", + residual_allocation_supply="passthrough", ) def test_nested_subclass_format(self, tmp_path: Path) -> None: - """Full nested format with delivery/supply/total.""" + """Delivery/supply format with separate blocks.""" rr_yaml = tmp_path / "nested.yaml" rr_yaml.write_text( yaml.safe_dump( @@ -160,17 +164,11 @@ def test_nested_subclass_format(self, tmp_path: Path) -> None: "total_delivery_revenue_requirement": 1000.0, "total_delivery_and_supply_revenue_requirement": 1500.0, "subclass_revenue_requirements": { - "percustomer": { - "hp": { - "delivery": 300.0, - "supply": 200.0, - "total": 500.0, - }, - "non-hp": { - "delivery": 700.0, - "supply": 300.0, - "total": 1000.0, - }, + "delivery": { + "percustomer": {"hp": 300.0, "non-hp": 700.0}, + }, + "supply": { + "passthrough": {"hp": 200.0, "non-hp": 300.0}, }, }, } @@ -186,7 +184,8 @@ def test_nested_subclass_format(self, tmp_path: Path) -> None: }, add_supply=False, run_includes_subclasses=True, - residual_allocation="percustomer", + residual_allocation_delivery="percustomer", + residual_allocation_supply="passthrough", ) assert result.rr_total == pytest.approx(1000.0) assert result.subclass_rr == {"hp_tariff": 300.0, "nonhp_tariff": 700.0} @@ -200,7 +199,8 @@ def test_nested_subclass_format(self, tmp_path: Path) -> None: }, add_supply=True, run_includes_subclasses=True, - residual_allocation="percustomer", + residual_allocation_delivery="percustomer", + residual_allocation_supply="passthrough", ) assert result_supply.rr_total == pytest.approx(1500.0) assert result_supply.subclass_rr == { diff --git a/tests/test_subclass_rr.py b/tests/test_subclass_rr.py index 3ae5e933..61119e0c 100644 --- a/tests/test_subclass_rr.py +++ b/tests/test_subclass_rr.py @@ -380,18 +380,16 @@ def test_write_revenue_requirement_yamls_two_runs(tmp_path: Path) -> None: ) sr = diff_data["subclass_revenue_requirements"] - assert "percustomer" in sr - pc = sr["percustomer"] + assert "delivery" in sr + assert "supply" in sr # Run 1 delivery: hp=400-7=393, non-hp=600-60=540 - # Run 2 total: hp=500-7=493, non-hp=700-60=640 (each bldg +50, same BAT) - # Supply = total - delivery - assert pc["hp"]["delivery"] == pytest.approx(393.0) - assert pc["hp"]["total"] == pytest.approx(493.0) - assert pc["hp"]["supply"] == pytest.approx(100.0) - assert pc["non-hp"]["delivery"] == pytest.approx(540.0) - assert pc["non-hp"]["total"] == pytest.approx(640.0) - assert pc["non-hp"]["supply"] == pytest.approx(100.0) + assert sr["delivery"]["percustomer"]["hp"] == pytest.approx(393.0) + assert sr["delivery"]["percustomer"]["non-hp"] == pytest.approx(540.0) + + # Supply pass-through: hp=(500-400)=100, non-hp=(700-600)=100 + assert sr["supply"]["passthrough"]["hp"] == pytest.approx(100.0) + assert sr["supply"]["passthrough"]["non-hp"] == pytest.approx(100.0) def test_write_revenue_requirement_yamls_round_trip(tmp_path: Path) -> None: @@ -433,22 +431,30 @@ def test_write_revenue_requirement_yamls_round_trip(tmp_path: Path) -> None: data = yaml.safe_load(differentiated_yaml.read_text(encoding="utf-8")) sr = data["subclass_revenue_requirements"] - - for method_key, method_block in sr.items(): - sum_delivery = sum(v["delivery"] for v in method_block.values()) - sum_total = sum(v["total"] for v in method_block.values()) - + assert "delivery" in sr + assert "supply" in sr + + # BAT-adjusted delivery methods should sum to total_delivery_rr. + # Passthrough sums to total bills (higher), so skip it. + for method_key, method_block in sr["delivery"].items(): + if method_key == "passthrough": + continue + sum_delivery = sum(method_block.values()) assert sum_delivery == pytest.approx( data["total_delivery_revenue_requirement"] - ), f"Method {method_key}: delivery sum mismatch" - assert sum_total == pytest.approx( - data["total_delivery_and_supply_revenue_requirement"] - ), f"Method {method_key}: total sum mismatch" + ), f"Delivery method {method_key}: sum mismatch" - for alias, vals in method_block.items(): - assert vals["total"] == pytest.approx(vals["delivery"] + vals["supply"]), ( - f"Method {method_key}, subclass {alias}: total != delivery + supply" - ) + # All supply methods should sum to total supply bills + # (passthrough = actual bills, percustomer/volumetric BAT sums to zero). + expected_supply = ( + data["total_delivery_and_supply_revenue_requirement"] + - data["total_delivery_revenue_requirement"] + ) + for method_key, method_block in sr["supply"].items(): + sum_supply = sum(method_block.values()) + assert sum_supply == pytest.approx(expected_supply), ( + f"Supply method {method_key}: sum mismatch" + ) def test_parse_group_value_to_subclass() -> None: @@ -1124,12 +1130,27 @@ def test_compute_hp_flat_discount_inputs_raises_when_negative_rate( # ============================================================================= -# Config parsing tests for nested YAML + residual_allocation +# Config parsing tests for delivery/supply YAML + residual_allocation # ============================================================================= - -def test_parse_nested_yaml_percustomer(tmp_path: Path) -> None: - """Nested YAML with residual_allocation=percustomer returns correct values.""" +_NEW_FORMAT_YAML = { + "total_delivery_revenue_requirement": 1000.0, + "total_delivery_and_supply_revenue_requirement": 1500.0, + "subclass_revenue_requirements": { + "delivery": { + "percustomer": {"hp": 100.0, "non-hp": 900.0}, + "epmc": {"hp": 120.0, "non-hp": 880.0}, + }, + "supply": { + "passthrough": {"hp": 50.0, "non-hp": 450.0}, + "percustomer": {"hp": 40.0, "non-hp": 460.0}, + }, + }, +} + + +def test_parse_delivery_percustomer_supply_passthrough(tmp_path: Path) -> None: + """Delivery percustomer + supply passthrough returns correct values.""" from utils.scenario_config import _parse_utility_revenue_requirement rr_yaml = tmp_path / "rr.yaml" @@ -1137,42 +1158,25 @@ def test_parse_nested_yaml_percustomer(tmp_path: Path) -> None: tariff_dir.mkdir() (tariff_dir / "rie_hp.json").write_text("{}") (tariff_dir / "rie_nonhp.json").write_text("{}") - - rr_yaml.write_text( - yaml.safe_dump( - { - "total_delivery_revenue_requirement": 1000.0, - "subclass_revenue_requirements": { - "percustomer": { - "hp": {"delivery": 100.0, "supply": 50.0, "total": 150.0}, - "non-hp": {"delivery": 900.0, "supply": 450.0, "total": 1350.0}, - }, - "epmc": { - "hp": {"delivery": 120.0, "supply": 60.0, "total": 180.0}, - "non-hp": {"delivery": 880.0, "supply": 440.0, "total": 1320.0}, - }, - }, - } - ) - ) + rr_yaml.write_text(yaml.safe_dump(_NEW_FORMAT_YAML)) raw_tariffs = {"hp": "tariffs/rie_hp.json", "non-hp": "tariffs/rie_nonhp.json"} config = _parse_utility_revenue_requirement( str(rr_yaml), tmp_path, raw_tariffs, - add_supply=False, + add_supply=True, run_includes_subclasses=True, - residual_allocation="percustomer", + residual_allocation_delivery="percustomer", + residual_allocation_supply="passthrough", ) assert config.subclass_rr is not None - assert config.subclass_rr["rie_hp"] == pytest.approx(100.0) - assert config.subclass_rr["rie_nonhp"] == pytest.approx(900.0) - assert config.residual_allocation == "percustomer" + assert config.subclass_rr["rie_hp"] == pytest.approx(150.0) + assert config.subclass_rr["rie_nonhp"] == pytest.approx(1350.0) -def test_parse_nested_yaml_epmc(tmp_path: Path) -> None: - """Nested YAML with residual_allocation=epmc returns EPMC values.""" +def test_parse_delivery_epmc_supply_passthrough(tmp_path: Path) -> None: + """Delivery EPMC + supply passthrough returns EPMC delivery + passthrough supply.""" from utils.scenario_config import _parse_utility_revenue_requirement rr_yaml = tmp_path / "rr.yaml" @@ -1180,24 +1184,33 @@ def test_parse_nested_yaml_epmc(tmp_path: Path) -> None: tariff_dir.mkdir() (tariff_dir / "rie_hp.json").write_text("{}") (tariff_dir / "rie_nonhp.json").write_text("{}") + rr_yaml.write_text(yaml.safe_dump(_NEW_FORMAT_YAML)) - rr_yaml.write_text( - yaml.safe_dump( - { - "total_delivery_revenue_requirement": 1000.0, - "subclass_revenue_requirements": { - "percustomer": { - "hp": {"delivery": 100.0, "supply": 50.0, "total": 150.0}, - "non-hp": {"delivery": 900.0, "supply": 450.0, "total": 1350.0}, - }, - "epmc": { - "hp": {"delivery": 120.0, "supply": 60.0, "total": 180.0}, - "non-hp": {"delivery": 880.0, "supply": 440.0, "total": 1320.0}, - }, - }, - } - ) + raw_tariffs = {"hp": "tariffs/rie_hp.json", "non-hp": "tariffs/rie_nonhp.json"} + config = _parse_utility_revenue_requirement( + str(rr_yaml), + tmp_path, + raw_tariffs, + add_supply=True, + run_includes_subclasses=True, + residual_allocation_delivery="epmc", + residual_allocation_supply="passthrough", ) + assert config.subclass_rr is not None + assert config.subclass_rr["rie_hp"] == pytest.approx(170.0) + assert config.subclass_rr["rie_nonhp"] == pytest.approx(1330.0) + + +def test_parse_delivery_only_ignores_supply(tmp_path: Path) -> None: + """Delivery-only run (add_supply=False) uses only delivery block.""" + from utils.scenario_config import _parse_utility_revenue_requirement + + rr_yaml = tmp_path / "rr.yaml" + tariff_dir = tmp_path / "tariffs" + tariff_dir.mkdir() + (tariff_dir / "rie_hp.json").write_text("{}") + (tariff_dir / "rie_nonhp.json").write_text("{}") + rr_yaml.write_text(yaml.safe_dump(_NEW_FORMAT_YAML)) raw_tariffs = {"hp": "tariffs/rie_hp.json", "non-hp": "tariffs/rie_nonhp.json"} config = _parse_utility_revenue_requirement( @@ -1206,15 +1219,16 @@ def test_parse_nested_yaml_epmc(tmp_path: Path) -> None: raw_tariffs, add_supply=False, run_includes_subclasses=True, - residual_allocation="epmc", + residual_allocation_delivery="percustomer", + residual_allocation_supply="passthrough", ) assert config.subclass_rr is not None - assert config.subclass_rr["rie_hp"] == pytest.approx(120.0) - assert config.subclass_rr["rie_nonhp"] == pytest.approx(880.0) + assert config.subclass_rr["rie_hp"] == pytest.approx(100.0) + assert config.subclass_rr["rie_nonhp"] == pytest.approx(900.0) -def test_parse_nested_yaml_unknown_allocation_raises(tmp_path: Path) -> None: - """Unknown residual_allocation raises ValueError listing available methods.""" +def test_parse_unknown_delivery_allocation_raises(tmp_path: Path) -> None: + """Unknown delivery allocation raises ValueError.""" from utils.scenario_config import _parse_utility_revenue_requirement rr_yaml = tmp_path / "rr.yaml" @@ -1222,46 +1236,37 @@ def test_parse_nested_yaml_unknown_allocation_raises(tmp_path: Path) -> None: tariff_dir.mkdir() (tariff_dir / "rie_hp.json").write_text("{}") (tariff_dir / "rie_nonhp.json").write_text("{}") - - rr_yaml.write_text( - yaml.safe_dump( - { - "total_delivery_revenue_requirement": 1000.0, - "subclass_revenue_requirements": { - "percustomer": { - "hp": {"delivery": 100.0, "supply": 50.0, "total": 150.0}, - "non-hp": {"delivery": 900.0, "supply": 450.0, "total": 1350.0}, - }, - }, - } - ) - ) + rr_yaml.write_text(yaml.safe_dump(_NEW_FORMAT_YAML)) raw_tariffs = {"hp": "tariffs/rie_hp.json", "non-hp": "tariffs/rie_nonhp.json"} - with pytest.raises(ValueError, match="residual_allocation='epcm'.*Available"): + with pytest.raises( + ValueError, match="residual_allocation_delivery='epcm'.*Available" + ): _parse_utility_revenue_requirement( str(rr_yaml), tmp_path, raw_tariffs, add_supply=False, run_includes_subclasses=True, - residual_allocation="epcm", + residual_allocation_delivery="epcm", + residual_allocation_supply="passthrough", ) -def test_parse_missing_residual_allocation_raises(tmp_path: Path) -> None: - """Missing residual_allocation for subclass run raises ValueError.""" +def test_parse_missing_delivery_allocation_raises(tmp_path: Path) -> None: + """Missing residual_allocation_delivery for subclass run raises ValueError.""" from utils.scenario_config import _parse_utility_revenue_requirement rr_yaml = tmp_path / "rr.yaml" rr_yaml.write_text(yaml.safe_dump({"total_delivery_revenue_requirement": 1000.0})) - with pytest.raises(ValueError, match="residual_allocation is not set"): + with pytest.raises(ValueError, match="residual_allocation_delivery.*is not set"): _parse_utility_revenue_requirement( str(rr_yaml), tmp_path, {}, add_supply=False, run_includes_subclasses=True, - residual_allocation=None, + residual_allocation_delivery=None, + residual_allocation_supply=None, ) diff --git a/utils/mid/compute_subclass_rr.py b/utils/mid/compute_subclass_rr.py index 73f6695d..da37e16b 100644 --- a/utils/mid/compute_subclass_rr.py +++ b/utils/mid/compute_subclass_rr.py @@ -37,7 +37,11 @@ DEFAULT_GROUP_COL = "has_hp" BAT_METRIC_CHOICES = ("BAT_vol", "BAT_peak", "BAT_percustomer", "BAT_epmc") DEFAULT_BAT_METRIC = "BAT_percustomer" -SUBCLASS_RR_ALLOCATION_METHODS: tuple[str, ...] = ("BAT_percustomer", "BAT_epmc") +SUBCLASS_RR_ALLOCATION_METHODS: tuple[str, ...] = ( + "BAT_percustomer", + "BAT_epmc", + "BAT_vol", +) BAT_COL_TO_ALLOCATION_KEY: dict[str, str] = { "BAT_percustomer": "percustomer", "BAT_epmc": "epmc", @@ -786,58 +790,91 @@ def _write_revenue_requirement_yamls( total_delivery_and_supply_rr: float | None = None, heating_type_breakdown: dict[str, dict[str, dict[str, float]]] | None = None, ) -> tuple[Path, Path]: - """Write per-subclass revenue requirement YAML with nested allocation methods. + """Write per-subclass revenue requirement YAML with separate delivery/supply blocks. - *delivery_breakdowns* is ``{bat_col: DataFrame}`` from run 1 (delivery-only). - *total_breakdowns*, when provided, is ``{bat_col: DataFrame}`` from run 2 - (delivery+supply). Supply per subclass is derived as total - delivery. + Delivery and supply allocation methods are independent. Each run picks + one delivery method and one supply method via ``residual_allocation_delivery`` + and ``residual_allocation_supply`` in its scenario YAML. - Output YAML structure:: + Delivery methods: passthrough, percustomer, epmc, volumetric. + Supply methods: passthrough, percustomer, volumetric. + (Supply EPMC is omitted — broken by the run 1/run 2 subtraction architecture; + volumetric gives a nearly identical result.) - subclass_revenue_requirements: - percustomer: - hp: {delivery: ..., supply: ..., total: ...} - non-hp: {delivery: ..., supply: ..., total: ...} - epmc: - hp: {delivery: ..., supply: ..., total: ...} - non-hp: {delivery: ..., supply: ..., total: ...} + Supply pass-through = actual supply bills per subclass (no BAT adjustment). + Supply percustomer/volumetric = supply bills - supply BAT (clean subtraction + because per-customer and volumetric weights are constant across runs). """ differentiated_yaml_path.parent.mkdir(parents=True, exist_ok=True) default_yaml_path.parent.mkdir(parents=True, exist_ok=True) gv_map = group_value_to_subclass or {} - all_methods_rr: dict[str, dict[str, dict[str, float]]] = {} + # --- Delivery block: BAT-adjusted RR per allocation method --- + delivery_block: dict[str, dict[str, float]] = {} + passthrough_delivery: dict[str, float] = {} for bat_col, delivery_breakdown in delivery_breakdowns.items(): method_key = BAT_COL_TO_ALLOCATION_KEY.get(bat_col, bat_col) - - total_rr_by_subclass: dict[str, float] = {} - if total_breakdowns is not None and bat_col in total_breakdowns: - for row in total_breakdowns[bat_col].to_dicts(): - total_rr_by_subclass[str(row["subclass"])] = float( - row["revenue_requirement"] - ) - - subclass_rr: dict[str, dict[str, float]] = {} + method_vals: dict[str, float] = {} for row in delivery_breakdown.to_dicts(): raw_val = str(row["subclass"]) alias = gv_map.get(raw_val, raw_val) - delivery = float(row["revenue_requirement"]) - total = total_rr_by_subclass.get(raw_val, delivery) - supply = total - delivery - subclass_rr[alias] = { - "delivery": delivery, - "supply": supply, - "total": total, - } + method_vals[alias] = float(row["revenue_requirement"]) + if not passthrough_delivery: + passthrough_delivery[alias] = float(row["sum_bills"]) + elif alias not in passthrough_delivery: + passthrough_delivery[alias] = float(row["sum_bills"]) + delivery_block[method_key] = method_vals + + delivery_block["passthrough"] = passthrough_delivery + + # --- Supply block: pass-through + BAT-adjusted methods --- + supply_block: dict[str, dict[str, float]] = {} + + if total_breakdowns is not None: + # Pass-through supply: actual supply bills per subclass (no BAT) + any_col = next(iter(delivery_breakdowns)) + del_bills: dict[str, float] = {} + for row in delivery_breakdowns[any_col].sort("subclass").to_dicts(): + raw_val = str(row["subclass"]) + alias = gv_map.get(raw_val, raw_val) + del_bills[alias] = float(row["sum_bills"]) - all_methods_rr[method_key] = subclass_rr + tot_bills: dict[str, float] = {} + for row in total_breakdowns[any_col].sort("subclass").to_dicts(): + raw_val = str(row["subclass"]) + alias = gv_map.get(raw_val, raw_val) + tot_bills[alias] = float(row["sum_bills"]) + + passthrough_supply = { + alias: tot_bills[alias] - del_bills[alias] for alias in del_bills + } + supply_block["passthrough"] = passthrough_supply + + # BAT-adjusted supply for methods with clean subtraction + # (percustomer and volumetric weights don't change between runs) + for bat_col in ("BAT_percustomer", "BAT_vol"): + method_key = BAT_COL_TO_ALLOCATION_KEY[bat_col] + if bat_col not in delivery_breakdowns or bat_col not in total_breakdowns: + continue + del_rr: dict[str, float] = {} + for row in delivery_breakdowns[bat_col].to_dicts(): + raw_val = str(row["subclass"]) + alias = gv_map.get(raw_val, raw_val) + del_rr[alias] = float(row["revenue_requirement"]) + tot_rr: dict[str, float] = {} + for row in total_breakdowns[bat_col].to_dicts(): + raw_val = str(row["subclass"]) + alias = gv_map.get(raw_val, raw_val) + tot_rr[alias] = float(row["revenue_requirement"]) + supply_block[method_key] = { + alias: tot_rr[alias] - del_rr[alias] for alias in del_rr + } differentiated_data: dict[str, object] = { "utility": utility, "group_col": group_col, - "allocation_methods": list(all_methods_rr.keys()), "source_run_dir": str(run_dir), } if total_delivery_rr is not None: @@ -846,7 +883,10 @@ def _write_revenue_requirement_yamls( differentiated_data["total_delivery_and_supply_revenue_requirement"] = ( total_delivery_and_supply_rr ) - differentiated_data["subclass_revenue_requirements"] = all_methods_rr + differentiated_data["subclass_revenue_requirements"] = { + "delivery": delivery_block, + "supply": supply_block, + } if heating_type_breakdown is not None: differentiated_data["heating_type_breakdown"] = heating_type_breakdown diff --git a/utils/post/validate/__main__.py b/utils/post/validate/__main__.py index 5917a841..72989109 100644 --- a/utils/post/validate/__main__.py +++ b/utils/post/validate/__main__.py @@ -421,9 +421,10 @@ def _record( ) total_rr = None + subclass_rr_raw = None subclass_rr = None if has_sub and total_rr is not None: - subclass_rr, sub_rr_ok = _safe_execute( + subclass_rr_raw, sub_rr_ok = _safe_execute( "load_revenue_requirement (subclass)", load_revenue_requirement, state, @@ -431,7 +432,32 @@ def _record( f"{utility}_hp_vs_nonhp.yaml", ) if not sub_rr_ok: - subclass_rr = None + subclass_rr_raw = None + if subclass_rr_raw is not None: + from utils.scenario_config import resolve_subclass_rr_for_validation + + try: + resolved = resolve_subclass_rr_for_validation( + subclass_rr_raw, "delivery" + ) + subclass_rr = { + "subclass_revenue_requirements": resolved, + } + if "total_delivery_revenue_requirement" in subclass_rr_raw: + subclass_rr["total_delivery_revenue_requirement"] = ( + subclass_rr_raw["total_delivery_revenue_requirement"] + ) + if ( + "total_delivery_and_supply_revenue_requirement" + in subclass_rr_raw + ): + subclass_rr["total_delivery_and_supply_revenue_requirement"] = ( + subclass_rr_raw[ + "total_delivery_and_supply_revenue_requirement" + ] + ) + except Exception: + subclass_rr = subclass_rr_raw for run_num, _, config, meta, bills in runs: run_dir = block_dir / f"run_{run_num}" diff --git a/utils/pre/create_scenario_yamls.py b/utils/pre/create_scenario_yamls.py index 63ac4593..0b78ed86 100644 --- a/utils/pre/create_scenario_yamls.py +++ b/utils/pre/create_scenario_yamls.py @@ -297,9 +297,12 @@ def parse_required_float(key: str) -> float: require_non_empty("run_includes_subclasses") ) - residual_allocation = get_optional("residual_allocation") - if residual_allocation: - run["residual_allocation"] = residual_allocation + residual_allocation_delivery = get_optional("residual_allocation_delivery") + if residual_allocation_delivery: + run["residual_allocation_delivery"] = residual_allocation_delivery + residual_allocation_supply = get_optional("residual_allocation_supply") + if residual_allocation_supply: + run["residual_allocation_supply"] = residual_allocation_supply run["path_electric_utility_stats"] = get("path_electric_utility_stats") diff --git a/utils/scenario_config.py b/utils/scenario_config.py index 67cdba82..d5b5d374 100644 --- a/utils/scenario_config.py +++ b/utils/scenario_config.py @@ -160,14 +160,17 @@ class RevenueRequirementConfig: rr_total: scalar for MC decomposition (delivery or delivery+supply). subclass_rr: per-tariff-key RR dict, or None for non-subclass runs. run_includes_subclasses: whether the run uses per-subclass RRs. - residual_allocation: the allocation method used (e.g. "percustomer", "epmc"), - or None for non-subclass runs. + residual_allocation_delivery: delivery allocation method (e.g. "percustomer", + "epmc"), or None for non-subclass runs. + residual_allocation_supply: supply allocation method (e.g. "passthrough", + "percustomer"), or None for non-subclass runs. """ rr_total: float subclass_rr: dict[str, float] | None run_includes_subclasses: bool - residual_allocation: str | None + residual_allocation_delivery: str | None + residual_allocation_supply: str | None def _parse_subclass_revenue_requirement( @@ -176,74 +179,95 @@ def _parse_subclass_revenue_requirement( base_dir: Path, *, add_supply: bool, - residual_allocation: str, + residual_allocation_delivery: str, + residual_allocation_supply: str, ) -> dict[str, float]: """Map subclass revenue requirements to tariff keys. - Expects nested YAML structure:: + Expects YAML structure with separate delivery and supply blocks:: subclass_revenue_requirements: - percustomer: - hp: {delivery: ..., supply: ..., total: ...} - non-hp: {delivery: ..., supply: ..., total: ...} - epmc: - hp: {delivery: ..., supply: ..., total: ...} - non-hp: {delivery: ..., supply: ..., total: ...} - - *residual_allocation* selects which allocation-method block to use - (e.g. ``"percustomer"`` or ``"epmc"``). Subclass alias keys - (``"hp"``/``"non-hp"``) match keys in *raw_path_tariffs_electric*. - Picks ``"delivery"`` or ``"total"`` per subclass based on *add_supply*. + delivery: + percustomer: {hp: ..., non-hp: ...} + epmc: {hp: ..., non-hp: ...} + supply: + passthrough: {hp: ..., non-hp: ...} + percustomer: {hp: ..., non-hp: ...} + + *residual_allocation_delivery* selects the delivery method. + *residual_allocation_supply* selects the supply method. + Subclass alias keys match keys in *raw_path_tariffs_electric*. + For delivery-only runs (add_supply=False), only delivery is used. + For delivery+supply runs, total = delivery + supply per subclass. """ raw_subclass = rr_data.get("subclass_revenue_requirements") if not isinstance(raw_subclass, dict) or not raw_subclass: raise ValueError("subclass_revenue_requirements must be a non-empty mapping") - if residual_allocation not in raw_subclass: - available = sorted(raw_subclass.keys()) - raise ValueError( - f"residual_allocation={residual_allocation!r} not found in " - f"subclass_revenue_requirements. Available: {available}" - ) + delivery_block = raw_subclass.get("delivery") + if not isinstance(delivery_block, dict) or not delivery_block: + raise ValueError("subclass_revenue_requirements must have a 'delivery' block") - subclass_rr = raw_subclass[residual_allocation] - if not isinstance(subclass_rr, dict) or not subclass_rr: + if residual_allocation_delivery not in delivery_block: + available = sorted(delivery_block.keys()) raise ValueError( - f"subclass_revenue_requirements[{residual_allocation!r}] " - "must be a non-empty mapping of subclass aliases" + f"residual_allocation_delivery={residual_allocation_delivery!r} " + f"not found in subclass_revenue_requirements.delivery. " + f"Available: {available}" ) + delivery_rr = delivery_block[residual_allocation_delivery] + alias_to_tariff_key = { alias: _resolve_path(str(path_str), base_dir).stem for alias, path_str in raw_path_tariffs_electric.items() } result: dict[str, float] = {} - for alias, amount in subclass_rr.items(): - alias_str = str(alias) + for alias_str, amount in delivery_rr.items(): tariff_key = alias_to_tariff_key.get(alias_str) if tariff_key is None: raise ValueError( f"Subclass alias {alias_str!r} in YAML not found in " f"path_tariffs_electric (available: {sorted(alias_to_tariff_key)})" ) - if not isinstance(amount, dict): + result[tariff_key] = _parse_float( + amount, + f"subclass_revenue_requirements.delivery" + f".{residual_allocation_delivery}.{alias_str}", + ) + + if add_supply: + supply_block = raw_subclass.get("supply") + if not isinstance(supply_block, dict) or not supply_block: raise ValueError( - f"subclass_revenue_requirements[{residual_allocation!r}]" - f"[{alias_str}] must be a dict with delivery/supply/total, " - f"got {type(amount).__name__}" + "subclass_revenue_requirements must have a 'supply' block " + "when add_supply=True" ) - rr_field = "total" if add_supply else "delivery" - if rr_field not in amount: + if residual_allocation_supply not in supply_block: + available = sorted(supply_block.keys()) raise ValueError( - f"subclass_revenue_requirements[{residual_allocation!r}]" - f"[{alias_str}] missing required field {rr_field!r}" + f"residual_allocation_supply={residual_allocation_supply!r} " + f"not found in subclass_revenue_requirements.supply. " + f"Available: {available}" + ) + supply_rr = supply_block[residual_allocation_supply] + for alias_str, amount in supply_rr.items(): + tariff_key = alias_to_tariff_key.get(alias_str) + if tariff_key is None: + raise ValueError( + f"Subclass alias {alias_str!r} in supply YAML not found in " + f"path_tariffs_electric (available: {sorted(alias_to_tariff_key)})" + ) + if tariff_key not in result: + raise ValueError( + f"Subclass {alias_str!r} in supply but not in delivery" + ) + result[tariff_key] += _parse_float( + amount, + f"subclass_revenue_requirements.supply" + f".{residual_allocation_supply}.{alias_str}", ) - result[tariff_key] = _parse_float( - amount[rr_field], - f"subclass_revenue_requirements[{residual_allocation!r}]" - f"[{alias_str}].{rr_field}", - ) return result @@ -255,7 +279,8 @@ def _parse_utility_revenue_requirement( *, add_supply: bool, run_includes_subclasses: bool = False, - residual_allocation: str | None = None, + residual_allocation_delivery: str | None = None, + residual_allocation_supply: str | None = None, ) -> RevenueRequirementConfig: """Parse utility_revenue_requirement from a YAML path. @@ -263,10 +288,10 @@ def _parse_utility_revenue_requirement( - rr_total: scalar from total_delivery[_and_supply]_revenue_requirement - subclass_rr: per-tariff-key RR dict (or None) - run_includes_subclasses: whether this run uses subclass RRs - - residual_allocation: which allocation method block was used + - residual_allocation_delivery / _supply: which methods were used - When *run_includes_subclasses* is True, *residual_allocation* is required - and selects the block within subclass_revenue_requirements to use. + When *run_includes_subclasses* is True, both *residual_allocation_delivery* + and *residual_allocation_supply* are required. """ if not isinstance(value, str): raise ValueError( @@ -311,10 +336,17 @@ def _parse_utility_revenue_requirement( subclass_rr: dict[str, float] | None = None if run_includes_subclasses: - if residual_allocation is None: + if residual_allocation_delivery is None: + raise ValueError( + "run_includes_subclasses is true but residual_allocation_delivery " + "is not set. Add 'residual_allocation_delivery: percustomer' " + "(or 'epmc') to the scenario YAML." + ) + if residual_allocation_supply is None: raise ValueError( - "run_includes_subclasses is true but residual_allocation is not set. " - "Add 'residual_allocation: percustomer' (or 'epmc') to the scenario YAML." + "run_includes_subclasses is true but residual_allocation_supply " + "is not set. Add 'residual_allocation_supply: passthrough' " + "(or 'percustomer') to the scenario YAML." ) if "subclass_revenue_requirements" not in rr_data: raise ValueError( @@ -326,14 +358,16 @@ def _parse_utility_revenue_requirement( raw_path_tariffs_electric, base_dir, add_supply=add_supply, - residual_allocation=residual_allocation, + residual_allocation_delivery=residual_allocation_delivery, + residual_allocation_supply=residual_allocation_supply, ) return RevenueRequirementConfig( rr_total=rr_total, subclass_rr=subclass_rr, run_includes_subclasses=run_includes_subclasses, - residual_allocation=residual_allocation, + residual_allocation_delivery=residual_allocation_delivery, + residual_allocation_supply=residual_allocation_supply, ) @@ -462,3 +496,29 @@ def _parse_bool(value: object, field_name: str) -> bool: raise ValueError( f"Invalid boolean for {field_name}: {value!r}. Use unquoted YAML true/false." ) + + +def resolve_subclass_rr_for_validation( + subclass_rr_data: dict[str, Any], + cost_scope: str, + residual_allocation_delivery: str = "percustomer", + residual_allocation_supply: str = "passthrough", +) -> dict[str, dict[str, float]]: + """Resolve the new delivery/supply YAML format into the old-style dict. + + Returns ``{alias: {"delivery": float, "supply": float, "total": float}}`` + for use by validation checks that expect the old format. + """ + raw = subclass_rr_data.get("subclass_revenue_requirements", subclass_rr_data) + delivery_block = raw.get("delivery", {}) + supply_block = raw.get("supply", {}) + + del_rr = delivery_block.get(residual_allocation_delivery, {}) + sup_rr = supply_block.get(residual_allocation_supply, {}) + + result: dict[str, dict[str, float]] = {} + for alias in del_rr: + d = float(del_rr[alias]) + s = float(sup_rr.get(alias, 0.0)) + result[alias] = {"delivery": d, "supply": s, "total": d + s} + return result From 57761c8f81f6b4191417bf35a6c27dae458472a8 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 16:00:37 +0000 Subject: [PATCH 18/40] [agents] cursor latex rendering --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 297ec687..017dd3eb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -130,6 +130,8 @@ Match existing style: Ruff for formatting/lint, **ty** for type checking, dprint **LaTeX in markdown:** GitHub's MathJax renderer does not support escaped underscores inside `\text{}` (e.g. `\text{avg\_mc\_peak}` will fail). Use proper math symbols instead: `\overline{MC}_{\text{peak}}`, `MC_h`, `L_h`, etc. Bare subscripts and `\text{}` with simple words (no underscores) are fine. +**LaTeX in Cursor chat:** When running inside Cursor, use `$$...$$` for display math and `$...$` for inline math. Cursor's chat renderer does not support `\[...\]` or `\(...\)` — the backslashes are consumed by the markdown parser, leaving bare brackets that the math renderer ignores. + ## Code Quality (required before every commit) - Run `just check` — no linter errors, no type errors, no warnings From 96760428347f2a7ce6b06de55a6daf521d045dfa Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 16:01:35 +0000 Subject: [PATCH 19/40] Settings: make cursor pyright evaluate fewer files --- .vscode/settings.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.vscode/settings.json b/.vscode/settings.json index 370079e3..ed925fdf 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -27,5 +27,15 @@ "**/config/rev_requirement/**", "dev_plots", "infra" + ], + "cursorpyright.analysis.diagnosticMode": "openFilesOnly", + "cursorpyright.analysis.exclude": [ + "run_logs", + "**/config/tariffs/**", + "**/config/scenarios/**", + "**/config/tariff_maps/**", + "**/config/rev_requirement/**", + "dev_plots", + "infra" ] } From 097a404d634c3b9d9b69711752ea38207d6afd98 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 13:10:13 -0400 Subject: [PATCH 20/40] Add EPMC and supply allocation context doc Comprehensive guide covering the "why" (EPMC primer, supply residual discovery), "what" (delivery/supply YAML split, four allocation methods, supply EPMC subtraction bug), and "how" (step-by-step NY testing plan with verification prompts). Made-with: Cursor --- context/README.md | 11 +- .../epmc_and_supply_allocation.md | 302 ++++++++++++++++++ 2 files changed, 308 insertions(+), 5 deletions(-) create mode 100644 context/methods/bat_mc_residual/epmc_and_supply_allocation.md diff --git a/context/README.md b/context/README.md index 3308a7e2..9c0ea314 100644 --- a/context/README.md +++ b/context/README.md @@ -46,11 +46,12 @@ Documents that answer **"How do we justify and operationalize this?"** — conce BAT framework, residual allocation, and how they connect to the literature. -| File | Purpose | -| ------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| bat_reasoning_stress_test.md | Formal reconstruction and econ-seminar-style stress-test of the BAT framework: marginal costs, residual allocation, the three allocators, cross-subsidy definitions (BAT vs strict economic), standalone vs incremental cost | -| bat_lrmc_residual_allocation_methodology.md | Our MC methodology as a standalone method: LRMC decomposition into energy SRMC + capacity FLIC, formulas for each component (energy LBMP, gen capacity ICAP, bulk TX incremental benefit, dist MCOS incremental diluted), how each becomes 8760, residual definition and allocation, with citations showing alignment to the BAT paper, Borenstein, Pérez-Arriaga, Schittekatte & Meeus | -| residual_allocation_lit_review_and_cairo.md | Residual allocation methods (volumetric, per-customer, peak, demand-based, historical consumption, Ramsey) from the cross-subsidization literature, mapped to CAIRO's three BAT allocators: formula, CAIRO status, literature survey by method, effect on solar/HP cross-subsidy. | +| File | Purpose | +| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| bat_reasoning_stress_test.md | Formal reconstruction and econ-seminar-style stress-test of the BAT framework: marginal costs, residual allocation, the three allocators, cross-subsidy definitions (BAT vs strict economic), standalone vs incremental cost | +| bat_lrmc_residual_allocation_methodology.md | Our MC methodology as a standalone method: LRMC decomposition into energy SRMC + capacity FLIC, formulas for each component (energy LBMP, gen capacity ICAP, bulk TX incremental benefit, dist MCOS incremental diluted), how each becomes 8760, residual definition and allocation, with citations showing alignment to the BAT paper, Borenstein, Pérez-Arriaga, Schittekatte & Meeus | +| epmc_and_supply_allocation.md | EPMC residual allocation and the delivery/supply split: why EPMC was added, the supply residual discovery (table of delivery vs supply residual % for all 8 utilities), the new YAML structure with separate delivery/supply blocks, four allocation methods (passthrough, volumetric, epmc, percustomer), the supply EPMC subtraction bug, current run configurations for RI and NY, and a self-contained mini-plan for an LLM to validate NY with exact commands and verification code | +| residual_allocation_lit_review_and_cairo.md | Residual allocation methods (volumetric, per-customer, peak, demand-based, historical consumption, Ramsey) from the cross-subsidization literature, mapped to CAIRO's three BAT allocators: formula, CAIRO status, literature survey by method, effect on solar/HP cross-subsidy. | ### methods/tou_and_rates/ diff --git a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md new file mode 100644 index 00000000..459064f8 --- /dev/null +++ b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md @@ -0,0 +1,302 @@ +# EPMC Residual Allocation and the Delivery/Supply Split + +This document explains what we added to the residual allocation pipeline in March 2026, why we added it, and how to test it for New York. It's written for a teammate who needs to run NY batches and verify the outputs. + +## 1. Why: EPMC and the supply residual problem + +### What is EPMC? + +EPMC stands for "equi-proportional marginal cost." It's a way of splitting the revenue requirement between customer subclasses (HP vs. non-HP). + +The basic idea: every customer's share of the residual is proportional to their share of total marginal costs. If HP customers cause 3% of total marginal costs, they bear 3% of the residual. Mathematically: + +$$R_i = R \times \frac{EB_i}{\sum_j EB_j \times w_j}$$ + +where $R$ is the total residual, $EB_i$ is customer $i$'s economic burden (sum of hourly load × hourly marginal price), and $w_j$ is the sample weight. + +This is equivalent to scaling all MC-based rates by a uniform constant $K = TRR / MC_{Revenue}$. Every customer's total bill is exactly $K$ times their economic burden. The markup ratio is the same for everyone. + +### Why did we add it? + +The existing per-customer allocation splits the residual equally across all customers, regardless of their load. This is the "lump-sum" approach — efficient in theory (no distortion of consumption decisions), but it doesn't reflect cost causation. Utilities and regulators accustomed to embedded cost-of-service (ECOS) analysis tend to prefer cost-causation-based allocation, where the residual is assigned to customers in proportion to how much system cost they drive. + +EPMC captures this perspective: if your load drives more marginal cost, you bear more residual. It's the standard reconciliation method used in California (CPUC) and is referenced in the BAT paper itself (Simeone et al. 2023, Appendix B, Eq. B3). + +Whether cost-causation is the _right_ normative principle for allocating sunk costs is debatable — see `context/domain/bat_mc_residual/fairness_in_cost_allocation.md` for the full discussion. The short version: historical cost causation = current cost causation is an assumption that is dubious both normatively (sunk costs shouldn't affect marginal decisions) and empirically (past investment decisions don't cleanly map to current load patterns). But it is the lens that most utility regulators actually use. + +### The supply residual problem + +When we implemented EPMC and started testing, we discovered something we hadn't fully appreciated: there are significant **supply-side residuals** at every utility. The supply residual is the gap between what customers pay for supply (retail supply charges × kWh) and the wholesale marginal cost of that supply (ISO energy LMP + capacity FCA/ICAP + ancillary). + +Here's how large the residual is, as a percentage of total revenue, for both delivery and supply: + +| Utility | Delivery residual | Supply residual | +| ------- | :---------------: | :-------------: | +| rie | 87% | 29% | +| cenhud | 96% | 27% | +| coned | 96% | 15% | +| nimo | 98% | 28% | +| nyseg | 98% | 36% | +| or | 95% | 30% | +| psegli | 93% | 27% | +| rge | 98% | 30% | + +Delivery residual is 87–98% of delivery revenue. This is well-known — delivery costs are dominated by infrastructure that's essentially a fixed budget, and marginal delivery costs are a small fraction of the total. + +Supply residual is 15–36% of supply revenue. This is less intuitive. The retail supply rate recovers more revenue than the wholesale marginal cost of supply. The gap covers things like supplier margin, hedging/procurement overhead, RECs, capacity contract premiums above the marginal clearing price, working capital, bad debt, and other costs baked into the retail supply rate. (Note: we are currently missing ancillary MCs for NY, which would shrink the supply residual somewhat.) + +**Open questions about the supply residual:** + +1. Is the residual "real" (i.e., do the retail supply rates genuinely include costs beyond energy/capacity/ancillary MCs), or is it partly an artifact of our MC inputs not capturing all supply cost components? +2. If the residual is real, what costs does it collect? Are those costs variable (they scale with consumption in time $t$, like MCs) or more like fixed budgets that have to be recovered regardless of load? +3. If they're fixed budgets, the same cross-subsidization logic applies to supply as to delivery — HP customers with higher kWh are bearing a disproportionate share of a fixed cost through the flat supply rate, just like they do on delivery. And then we need to decide how to allocate the supply residual, just like we do on delivery. + +We don't have definitive answers to these questions yet. For now, the supply residual is treated as a design parameter — each run can choose how (or whether) to allocate it. + +## 2. What: the delivery/supply allocation split + +### The architecture change + +Previously, `compute_subclass_rr` produced a single set of subclass revenue requirements (delivery + supply combined) under each allocation method. The supply component was derived by subtracting run 1 (delivery-only) from run 2 (delivery+supply). + +The problem: this subtraction is only clean when the allocation weights are the same in both runs. For per-customer and volumetric allocation, they are — customer count and kWh don't change between runs. For EPMC, they're not — HP's share of economic burden is 2.67% in the delivery-only run vs. 4.83% in the combined run (because adding supply MCs changes the EB mix). The subtraction produces a "supply EPMC" number that doesn't correspond to any principled supply-only allocation. In RI, this inflated HP's supply RR by $10M, making the HP flat rate look terrible when it should have looked good. + +The fix: the subclass RR YAML now has **separate delivery and supply blocks**. Each run picks its delivery allocation method and supply allocation method independently. + +### The new YAML structure + +```yaml +subclass_revenue_requirements: + delivery: + passthrough: # no cross-subsidy correction + hp: 23816775 + non-hp: 518739813 + percustomer: # corrects MC + residual cross-subsidy + hp: 11538113 + non-hp: 531018474 + epmc: # allocates residual by MC share + hp: 14510936 + non-hp: 528045651 + volumetric: # allocates residual by kWh + hp: ... + non-hp: ... + supply: + passthrough: # same supply rate as default — no correction at all + hp: 21129827 + non-hp: 391820543 + percustomer: # corrects supply MC + residual cross-subsidy + hp: 18175699 + non-hp: 394774670 + volumetric: # corrects supply MC cross-subsidy, not residual + hp: ... + non-hp: ... + # No EPMC: broken by the subtraction architecture. Volumetric gives + # nearly the same answer (within $1M for RI). +``` + +Each scenario run in the YAML picks its methods: + +```yaml +residual_allocation_delivery: epmc +residual_allocation_supply: passthrough +``` + +The parser composes the total: `total_RR[subclass] = delivery[method_d][subclass] + supply[method_s][subclass]`. + +### The four allocation methods and what they do + +Here's what each method does, concretely, when you apply it to a subclass: + +**Passthrough** leaves the cross-subsidy intact on both the MC and the residual. HP's subclass RR equals their actual bills under the default flat rate. CAIRO then calibrates the HP tariff to recover those same bills — so the HP rate ends up being the same as the default rate. No correction at all. Use this when you don't want to touch that side of the bill (e.g., supply-side for a delivery-only rate design). + +**Volumetric** fixes the MC cross-subsidy but leaves the residual allocated by kWh (same as the flat rate). HP's subclass RR = HP's economic burden + (HP kWh share × total residual). If HP's average MC per kWh is below the system average (true on supply side, where capacity peaks in summer and HP load peaks in winter), HP gets a lower subclass RR than passthrough. If HP's average MC per kWh is above average (true on delivery side in summer-peaking systems), HP gets a higher subclass RR. + +**EPMC** fixes the MC cross-subsidy and allocates the residual proportional to each subclass's share of total MC. This usually gives HP a lower total residual allocation than volumetric (because HP's MC share is typically less than their kWh share on supply, and the reverse on delivery). On delivery, where residual is 87–98% of costs, the allocation method matters enormously. On supply, where residual is 15–36%, the difference between EPMC and volumetric is small. + +**Per-customer** fixes the MC cross-subsidy and allocates the residual evenly per person. This removes the volumetric distortion the most — HP customers with high kWh no longer bear a disproportionate share of the fixed-cost residual. It ends up with the lowest HP subclass cost allocation for both delivery and supply, because HP customers are a small fraction of total customer count (2% in RI). + +### How much the allocation method matters: delivery vs. supply + +The impact of the allocation method depends on how large the residual is relative to total costs. On delivery, where 87–98% of the bill is residual, the allocation method is decisive — it determines almost the entire subclass RR. On supply, where 15–36% is residual, the allocation method matters but the MC component dominates. + +| Utility | Delivery residual % | Supply residual % | +| ------- | :-----------------: | :---------------: | +| rie | 87% | 29% | +| cenhud | 96% | 27% | +| coned | 96% | 15% | +| nimo | 98% | 28% | +| nyseg | 98% | 36% | +| or | 95% | 30% | +| psegli | 93% | 27% | +| rge | 98% | 30% | + +This is why we didn't bother with volumetric on the delivery side until now — when 98% of the cost is residual, the MC correction is almost irrelevant; what matters is how you allocate the residual. On the supply side, the MC correction is more meaningful (it's 64–85% of the bill), and the residual allocation method matters less. + +### The supply EPMC bug + +Supply-side EPMC is currently disabled in the pipeline. Here's why. + +CAIRO runs separately for delivery-only (run 1) and delivery+supply (run 2). We derive the supply component by subtraction: supply = run 2 - run 1. For EPMC, the allocation weights (economic burden shares) are different in each run: + +- Run 1 (delivery only): HP is 2.67% of total EB +- Run 2 (delivery+supply): HP is 4.83% of total EB + +The subtraction `EPMC(run 2) - EPMC(run 1)` produces a number that uses mixed weights. In RI, this created a phantom $15.9M "supply EPMC residual" for HP, when the correct value (computed independently using supply-only EB) is ~$6.4M. The $10M error inflated HP's total RR from $30M to $46M. + +Per-customer and volumetric don't have this problem because their weights (customer count, kWh) are the same in both runs. Volumetric supply gives nearly the same answer as correct supply EPMC (within $1M for RI), so we use volumetric as a proxy. + +Actually implementing correct supply EPMC would require decomposing run 2's economic burden into delivery-only and supply-only EB per customer (using the separate delivery and supply MC traces that CAIRO already has), then doing two separate EPMC allocations within a single run. This is a change to CAIRO's postprocessor — maybe 50 lines of code, but it changes the semantics of what BAT_epmc means in run 2 and needs careful validation. + +### What we're actually doing right now + +For **RI**, we're only changing runs 17–18 (HP flat rate). These use EPMC delivery + passthrough supply. All other RI subclass runs (5–6 seasonal, 9–10 TOU, 13–14 TOU flex) keep per-customer for both delivery and supply, same as before. + +| RI runs | Rate design | Delivery | Supply | +| --------- | ----------- | ----------- | --------------- | +| 5–6 | HP seasonal | percustomer | percustomer | +| 9–10 | HP TOU | percustomer | percustomer | +| 13–14 | HP TOU flex | percustomer | percustomer | +| **17–18** | **HP flat** | **epmc** | **passthrough** | + +For **NY**, nothing changes yet. All subclass runs use per-customer for both: + +| NY runs | Rate design | Delivery | Supply | +| ------- | ----------- | ----------- | ----------- | +| 5–6 | HP seasonal | percustomer | percustomer | +| 9–10 | HP TOU | percustomer | percustomer | +| 13–14 | HP TOU flex | percustomer | percustomer | + +**What's likely to change for NY:** + +- We'll probably move NY delivery allocation to EPMC as well. +- We may end up making the "fair rate" for NY a flat HP rate instead of seasonal. +- We still need to decide what the supply-side allocations should be — whether to move them to EPMC as well (which would mean volumetric in practice, since supply EPMC is broken and volumetric is a close proxy), or keep per-customer. + +All of these choices come later. For now, NY is unchanged. The delivery and supply allocation methods for each run are specified in the Google Sheet's `residual_allocation_delivery` and `residual_allocation_supply` columns, and flow through to the scenario YAML via `just create-scenario-yamls`. + +## 3. How: testing NY + +The previous section explains the theory. This section tells you exactly how to run a NY batch and verify the outputs. We've already run RI with these changes. NY is next, and should produce identical results to the old batch for all runs (since NY still uses per-customer for everything). + +### What was implemented (high level) + +1. **CAIRO monkey-patches** (`utils/mid/patches.py`): added EPMC residual allocation, disabled broken peak allocation, added BAT_epmc to the cross-subsidization CSV. + +2. **`compute_subclass_rr.py`**: computes all delivery and supply allocation methods in a single pass and writes the new YAML structure with separate delivery/supply blocks. + +3. **`scenario_config.py` + `run_scenario.py`**: the parser reads `residual_allocation_delivery` and `residual_allocation_supply` from the scenario YAML, composes the total RR from the delivery + supply blocks. + +4. **`build_master_bat.py`**: detects available BAT metrics at runtime — gracefully handles missing BAT_peak and new BAT_epmc columns. + +5. **`create_scenario_yamls.py`**: reads `residual_allocation_delivery` and `residual_allocation_supply` columns from the Google Sheet. + +The key insight: all the subclass RR values (for every combination of allocation method) are computed once by `compute-rev-requirements` and written to a single YAML file. Each run then picks which delivery and supply method to use, via its scenario YAML. So testing is: run `compute-rev-requirements`, check the YAML, run the batch, check the outputs. + +### Step-by-step: run and verify a NY batch + +#### Prerequisites + +1. Make sure the Google Sheet has `residual_allocation_delivery` and `residual_allocation_supply` columns. NY runs 5, 6, 9, 10, 13, 14 should have `percustomer` for both. All other runs: blank. + +2. On the server, pull the latest code: + ```bash + cd /ebs/home/jpv_switch_box/rate-design-platform + git pull + ``` + +3. If scenario YAMLs were regenerated from the sheet, verify them: + ```bash + grep -n "residual_allocation" rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml + ``` + Runs 5, 6, 9, 10, 13, 14 should show `residual_allocation_delivery: percustomer` and `residual_allocation_supply: percustomer`. + +#### Run the batch + +```bash +cd /ebs/home/jpv_switch_box/rate-design-platform/rate_design/hp_rates + +# Run all-pre for NY (regenerates RR YAMLs with new delivery/supply structure) +just s ny all-pre + +# Verify one RR YAML has the new structure +cat ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml | head -30 +# Should have "delivery:" and "supply:" blocks, each with +# "passthrough:", "percustomer:", "volumetric:" sub-blocks. + +# Run all utilities +for util in or cenhud rge nyseg psegli nimo coned; do + echo ">> Running $util" + UTILITY=$util RDP_BATCH=ny_YYYYMMDD_r1-16_epmc just s ny run-all-parallel-tracks +done + +# Build all master bills + BATs +just s ny build-all-master ny_YYYYMMDD_r1-16_epmc +``` + +(Replace `YYYYMMDD` with today's date.) + +#### Manual verification: the YAML + +After `just s ny all-pre`, check one of the RR YAMLs: + +```bash +cat ny/config/rev_requirement/cenhud_hp_vs_nonhp.yaml +``` + +You should see: + +- A `subclass_revenue_requirements:` key with `delivery:` and `supply:` sub-blocks +- Under `delivery:`, keys for `passthrough`, `percustomer`, `epmc`, `volumetric` +- Under `supply:`, keys for `passthrough`, `percustomer`, `volumetric` +- Each contains `hp:` and `non-hp:` with scalar dollar values +- `percustomer` delivery values should match the old YAML's `hp.delivery` and `non-hp.delivery` values (since NY runs use per-customer delivery) + +#### LLM verification: master BAT and bills + +After the batch completes and master bills/BATs are built, have an LLM run the following verification. Copy-paste this entire block into a chat with an LLM that has access to the rate-design-platform repo and AWS: + +--- + +**VERIFICATION PROMPT FOR LLM:** + +I just ran a NY batch called `ny_YYYYMMDD_r1-16_epmc` (replace with actual batch name). The old batch to compare against is `ny_20260325b_r1-16`. Please run the following checks and report results. + +The batch is at `s3://data.sb/switchbox/cairo/outputs/hp_rates/ny/all_utilities/`. Run pairs are: 1+2, 3+4, 5+6, 7+8, 9+10, 11+12, 13+14, 15+16. + +**Check 1: New columns present, old columns absent.** + +For each run pair, read the master BAT parquet and verify: + +- `BAT_epmc_delivery` is present (new EPMC column) +- `BAT_peak_delivery` is NOT present (peak was disabled) +- `residual_share_epmc_delivery` is present + +Also read the master bills parquet and verify LMI columns exist (e.g., `elec_total_bill_lmi_100` or `elec_total_bill_lmi_40`). + +**Check 2: All run pairs 1–12 BAT unchanged vs old batch.** + +For each of run pairs 1+2, 3+4, 5+6, 7+8, 9+10, 11+12: join old and new master BAT on `bldg_id` + `sb.electric_utility`. For every shared numeric column, the max absolute difference should be less than 1e-6. Report any column that exceeds this tolerance. + +**Check 3: All run pairs 1–12 bills unchanged vs old batch.** + +Same as check 2 but for master bills. Join on `bldg_id` + `sb.electric_utility` + `month`. All shared numeric columns should match within 1e-6. + +**Check 4: Run pairs 13–14 and 15–16 may differ.** + +These are the demand-flex runs. The CAIRO patches (peak removal, EPMC addition) affect the frozen-residual decomposition in the demand-flex code path. Small diffs in runs 13–14 and larger diffs in 15–16 (which inherit the calibrated tariff from 13–14) are expected. Report the diffs but don't treat them as failures. + +**Check 5: Building counts consistent.** + +For each utility, verify the building count is the same across all 8 run pairs. + +**Expected results:** + +- Check 1: all OK (epmc present, peak absent, LMI cols present) +- Check 2: all OK for runs 1–12 +- Check 3: all OK for runs 1–12 +- Check 4: runs 13–16 may show diffs (expected) +- Check 5: all OK + +--- + +That verification prompt is self-contained — the LLM should be able to run it using `polars` to read the S3 parquets and report results. The key thing you're looking for is that runs 1–12 are identical to the old batch (no behavioral change for NY), while runs 13–16 may differ slightly due to the demand-flex interaction with the CAIRO patches. From f7a845f7ceb700a7adddddd81755cdd2ba9db072 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 17:11:53 +0000 Subject: [PATCH 21/40] Add supply and delivery residual allocations to RIE scenarios --- .../ri/config/scenarios/scenarios_rie.yaml | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml b/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml index ae68e154..e6f13ead 100644 --- a/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml +++ b/rate_design/hp_rates/ri/config/scenarios/scenarios_rie.yaml @@ -133,6 +133,8 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -140,8 +142,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation_delivery: percustomer - residual_allocation_supply: passthrough 6: run_name: ri_rie_run6_up00_precalc_supply__hp_seasonal_vs_default state: RI @@ -164,6 +164,8 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -171,8 +173,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation_delivery: percustomer - residual_allocation_supply: passthrough 7: run_name: ri_rie_run7_up02_default__hp_seasonal state: RI @@ -251,6 +251,8 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -258,8 +260,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation_delivery: percustomer - residual_allocation_supply: percustomer 10: run_name: ri_rie_run10_up00_precalc_supply__hp_seasonalTOU_vs_default state: RI @@ -282,6 +282,8 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -289,8 +291,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation_delivery: percustomer - residual_allocation_supply: percustomer 11: run_name: ri_rie_run11_up02_default__hp_seasonalTOU state: RI @@ -369,6 +369,8 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -376,8 +378,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 - residual_allocation_delivery: percustomer - residual_allocation_supply: percustomer 14: run_name: ri_rie_run14_up00_precalc_supply__hp_seasonalTOU_flex_vs_default state: RI @@ -400,6 +400,8 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation_delivery: percustomer + residual_allocation_supply: percustomer path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -407,8 +409,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: -0.1 - residual_allocation_delivery: percustomer - residual_allocation_supply: percustomer 15: run_name: ri_rie_run15_up02_default__hp_seasonalTOU_flex state: RI @@ -487,6 +487,8 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: false run_includes_subclasses: true + residual_allocation_delivery: epmc + residual_allocation_supply: passthrough path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -494,8 +496,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation_delivery: epmc - residual_allocation_supply: passthrough 18: run_name: ri_rie_run18_up00_precalc_supply__hp_flat_vs_default state: RI @@ -518,6 +518,8 @@ runs: utility_revenue_requirement: rev_requirement/rie_hp_vs_nonhp.yaml run_includes_supply: true run_includes_subclasses: true + residual_allocation_delivery: epmc + residual_allocation_supply: passthrough path_electric_utility_stats: s3://data.sb/eia/861/electric_utility_stats/year=2024/state=RI/data.parquet path_bulk_tx_mc: s3://data.sb/switchbox/marginal_costs/ri/bulk_tx/utility=rie/year=2025/data.parquet solar_pv_compensation: net_metering @@ -525,8 +527,6 @@ runs: year_dollar_conversion: 2025 process_workers: 8 elasticity: 0.0 - residual_allocation_delivery: epmc - residual_allocation_supply: passthrough 19: run_name: ri_rie_run19_up02_default__hp_flat state: RI From f87845d0c22d3a4f3593feeae5ca7c03ea494b99 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 14:18:02 -0400 Subject: [PATCH 22/40] Fix framing of architecture change in EPMC context doc The delivery/supply YAML split solves the combinatorial problem (needing different allocation methods for delivery vs supply), not the EPMC subtraction bug. Rewrite to make this distinction clear. Made-with: Cursor --- .../epmc_and_supply_allocation.md | 96 ++++++++++--------- 1 file changed, 49 insertions(+), 47 deletions(-) diff --git a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md index 459064f8..2129135d 100644 --- a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md +++ b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md @@ -1,6 +1,6 @@ # EPMC Residual Allocation and the Delivery/Supply Split -This document explains what we added to the residual allocation pipeline in March 2026, why we added it, and how to test it for New York. It's written for a teammate who needs to run NY batches and verify the outputs. +This document explains what we added to the residual allocation pipeline the week of March 23rd 2026, why we added it, and how to test it for New York. It's written for a teammate who needs to run NY batches and verify the outputs. ## 1. Why: EPMC and the supply residual problem @@ -8,21 +8,21 @@ This document explains what we added to the residual allocation pipeline in Marc EPMC stands for "equi-proportional marginal cost." It's a way of splitting the revenue requirement between customer subclasses (HP vs. non-HP). -The basic idea: every customer's share of the residual is proportional to their share of total marginal costs. If HP customers cause 3% of total marginal costs, they bear 3% of the residual. Mathematically: +The basic idea: every customer's (or subclass's) share of the residual is proportional to their share of total marginal costs. If HP customers cause 3% of total marginal costs, they bear 3% of the residual. Mathematically: $$R_i = R \times \frac{EB_i}{\sum_j EB_j \times w_j}$$ where $R$ is the total residual, $EB_i$ is customer $i$'s economic burden (sum of hourly load × hourly marginal price), and $w_j$ is the sample weight. -This is equivalent to scaling all MC-based rates by a uniform constant $K = TRR / MC_{Revenue}$. Every customer's total bill is exactly $K$ times their economic burden. The markup ratio is the same for everyone. +This is equivalent to scaling all MC-based rates by a uniform constant $K = TRR / MC_{Revenue}$. Every customer's total bill is exactly $K$ times their economic burden. The markup ratio is the same for everyone. (This is why applying EPMC residual allocation to individual customers or to the subclasses as a whole gives you the same result in terms of total subclass residual.) ### Why did we add it? -The existing per-customer allocation splits the residual equally across all customers, regardless of their load. This is the "lump-sum" approach — efficient in theory (no distortion of consumption decisions), but it doesn't reflect cost causation. Utilities and regulators accustomed to embedded cost-of-service (ECOS) analysis tend to prefer cost-causation-based allocation, where the residual is assigned to customers in proportion to how much system cost they drive. +The existing per-customer allocation splits the residual equally across all customers, regardless of their load. This is the "lump-sum" approach (see (NARUC, 1992, p. 162) — efficient in theory (no distortion of consumption decisions), but it doesn't reflect cost causation. Utilities and regulators accustomed to embedded cost-of-service (ECOS) analysis tend to prefer cost-causation-based allocation, where the residual is assigned to customers in proportion to how much system cost they drive. -EPMC captures this perspective: if your load drives more marginal cost, you bear more residual. It's the standard reconciliation method used in California (CPUC) and is referenced in the BAT paper itself (Simeone et al. 2023, Appendix B, Eq. B3). +EPMC captures this perspective: if your load drives more marginal cost, you bear more residual. It's the standard reconciliation method used in California (CPUC) and is referenced in the BAT paper itself (Simeone et al. 2023, Appendix B, Eq. B3) and in NARUC's Electric Utility Cost Alloation Manual (NARUC, 1992, p. 160). -Whether cost-causation is the _right_ normative principle for allocating sunk costs is debatable — see `context/domain/bat_mc_residual/fairness_in_cost_allocation.md` for the full discussion. The short version: historical cost causation = current cost causation is an assumption that is dubious both normatively (sunk costs shouldn't affect marginal decisions) and empirically (past investment decisions don't cleanly map to current load patterns). But it is the lens that most utility regulators actually use. +Whether cost-causation is the _right_ normative principle for allocating sunk costs is debatable — see `context/domain/bat_mc_residual/fairness_in_cost_allocation.md` for the full discussion. The short version: historical cost causation = current cost causation is an assumption that is dubious both normatively (the beneficiary-pays principle points towards cost-causation-based allocation for marginal costs but away from it for residual costs), instrumentally (sunk costs shouldn't affect marginal decisions, and they are if cost-causation-allocated residual costs are recovered through volumetric charges) and empirically (past investment decisions don't cleanly map to current load patterns). But it is the lens that most utility regulators actually use. ### The supply residual problem @@ -31,25 +31,29 @@ When we implemented EPMC and started testing, we discovered something we hadn't Here's how large the residual is, as a percentage of total revenue, for both delivery and supply: | Utility | Delivery residual | Supply residual | -| ------- | :---------------: | :-------------: | -| rie | 87% | 29% | -| cenhud | 96% | 27% | -| coned | 96% | 15% | -| nimo | 98% | 28% | -| nyseg | 98% | 36% | -| or | 95% | 30% | -| psegli | 93% | 27% | -| rge | 98% | 30% | +| ------- | ----------------- | --------------- | +| rie | 87% | 29% | +| cenhud | 96% | 27% | +| coned | 96% | 15% | +| nimo | 98% | 28% | +| nyseg | 98% | 36% | +| or | 95% | 30% | +| psegli | 93% | 27% | +| rge | 98% | 30% | Delivery residual is 87–98% of delivery revenue. This is well-known — delivery costs are dominated by infrastructure that's essentially a fixed budget, and marginal delivery costs are a small fraction of the total. -Supply residual is 15–36% of supply revenue. This is less intuitive. The retail supply rate recovers more revenue than the wholesale marginal cost of supply. The gap covers things like supplier margin, hedging/procurement overhead, RECs, capacity contract premiums above the marginal clearing price, working capital, bad debt, and other costs baked into the retail supply rate. (Note: we are currently missing ancillary MCs for NY, which would shrink the supply residual somewhat.) +Supply residual is 15–36% of supply revenue. This is less intuitive. The retail supply rate recovers more revenue than the wholesale marginal cost of supply. + +We're still trying to understand how much of this "residual" might be due to simple data quality issues (inaccuracies in our retail rates [which are multiplied by EIA kwh to become the supply RR] or our marginal costs, missing marginal costs [such as ancillary services in NY, RECs in RI and NY), temporal mis-alignment between our retail rates / kwhs / marginal costs, etc.). + +Beyond that, the gap covers things like supplier margin, hedging/procurement overhead, RECs, capacity contract premiums above the marginal clearing price, working capital, bad debt, and other costs baked into the retail supply rate. **Open questions about the supply residual:** -1. Is the residual "real" (i.e., do the retail supply rates genuinely include costs beyond energy/capacity/ancillary MCs), or is it partly an artifact of our MC inputs not capturing all supply cost components? +1. Is the residual "real" (i.e., do the retail supply rates genuinely include costs beyond energy/capacity/ancillary MCs), or is it partly of the data quality or missing data issues described above? 2. If the residual is real, what costs does it collect? Are those costs variable (they scale with consumption in time $t$, like MCs) or more like fixed budgets that have to be recovered regardless of load? -3. If they're fixed budgets, the same cross-subsidization logic applies to supply as to delivery — HP customers with higher kWh are bearing a disproportionate share of a fixed cost through the flat supply rate, just like they do on delivery. And then we need to decide how to allocate the supply residual, just like we do on delivery. +3. If they're fixed budgets, the same cross-subsidization logic applies to supply as to delivery — HP customers with higher kWh are bearing a disproportionate share of a fixed cost through the flat supply rate, just like they do on delivery. And then we need to decide how to allocate the supply residual, just like we do on delivery. Why residual allocation method makes sense here? We don't have definitive answers to these questions yet. For now, the supply residual is treated as a design parameter — each run can choose how (or whether) to allocate it. @@ -57,11 +61,11 @@ We don't have definitive answers to these questions yet. For now, the supply res ### The architecture change -Previously, `compute_subclass_rr` produced a single set of subclass revenue requirements (delivery + supply combined) under each allocation method. The supply component was derived by subtracting run 1 (delivery-only) from run 2 (delivery+supply). +Previously, `compute_subclass_rr` produced a single set of subclass revenue requirements under one allocation method (per-customer), and the delivery/supply split was implicit — supply was derived by subtracting run 1 (delivery-only) from run 2 (delivery+supply). Every run got the same allocation method for both delivery and supply; there was no way to say "use EPMC for delivery but passthrough for supply." -The problem: this subtraction is only clean when the allocation weights are the same in both runs. For per-customer and volumetric allocation, they are — customer count and kWh don't change between runs. For EPMC, they're not — HP's share of economic burden is 2.67% in the delivery-only run vs. 4.83% in the combined run (because adding supply MCs changes the EB mix). The subtraction produces a "supply EPMC" number that doesn't correspond to any principled supply-only allocation. In RI, this inflated HP's supply RR by $10M, making the HP flat rate look terrible when it should have looked good. +Once we added EPMC and passthrough, we needed the ability to mix and match. RI's HP flat rate needs EPMC delivery + passthrough supply. RI's HP seasonal rate needs per-customer delivery + per-customer supply. NY might want something different again. The old single-method structure couldn't express this. -The fix: the subclass RR YAML now has **separate delivery and supply blocks**. Each run picks its delivery allocation method and supply allocation method independently. +The fix: `compute_subclass_rr` now precomputes every available allocation method for both delivery and supply, and writes them all to the YAML. Each run then picks which delivery method and which supply method to use, via `residual_allocation_delivery` and `residual_allocation_supply` in its scenario YAML. The YAML stores all the options; the run selects the combination it needs. ### The new YAML structure @@ -120,15 +124,15 @@ Here's what each method does, concretely, when you apply it to a subclass: The impact of the allocation method depends on how large the residual is relative to total costs. On delivery, where 87–98% of the bill is residual, the allocation method is decisive — it determines almost the entire subclass RR. On supply, where 15–36% is residual, the allocation method matters but the MC component dominates. | Utility | Delivery residual % | Supply residual % | -| ------- | :-----------------: | :---------------: | -| rie | 87% | 29% | -| cenhud | 96% | 27% | -| coned | 96% | 15% | -| nimo | 98% | 28% | -| nyseg | 98% | 36% | -| or | 95% | 30% | -| psegli | 93% | 27% | -| rge | 98% | 30% | +| ------- | ------------------- | ----------------- | +| rie | 87% | 29% | +| cenhud | 96% | 27% | +| coned | 96% | 15% | +| nimo | 98% | 28% | +| nyseg | 98% | 36% | +| or | 95% | 30% | +| psegli | 93% | 27% | +| rge | 98% | 30% | This is why we didn't bother with volumetric on the delivery side until now — when 98% of the cost is residual, the MC correction is almost irrelevant; what matters is how you allocate the residual. On the supply side, the MC correction is more meaningful (it's 64–85% of the bill), and the residual allocation method matters less. @@ -181,14 +185,10 @@ The previous section explains the theory. This section tells you exactly how to ### What was implemented (high level) 1. **CAIRO monkey-patches** (`utils/mid/patches.py`): added EPMC residual allocation, disabled broken peak allocation, added BAT_epmc to the cross-subsidization CSV. - -2. **`compute_subclass_rr.py`**: computes all delivery and supply allocation methods in a single pass and writes the new YAML structure with separate delivery/supply blocks. - -3. **`scenario_config.py` + `run_scenario.py`**: the parser reads `residual_allocation_delivery` and `residual_allocation_supply` from the scenario YAML, composes the total RR from the delivery + supply blocks. - -4. **`build_master_bat.py`**: detects available BAT metrics at runtime — gracefully handles missing BAT_peak and new BAT_epmc columns. - -5. **`create_scenario_yamls.py`**: reads `residual_allocation_delivery` and `residual_allocation_supply` columns from the Google Sheet. +2. `**compute_subclass_rr.py`**: computes all delivery and supply allocation methods in a single pass and writes the new YAML structure with separate delivery/supply blocks. +3. `**scenario_config.py` + `run_scenario.py**`: the parser reads `residual_allocation_delivery` and `residual_allocation_supply` from the scenario YAML, composes the total RR from the delivery + supply blocks. +4. `**build_master_bat.py**`: detects available BAT metrics at runtime — gracefully handles missing BAT_peak and new BAT_epmc columns. +5. `**create_scenario_yamls.py**`: reads `residual_allocation_delivery` and `residual_allocation_supply` columns from the Google Sheet. The key insight: all the subclass RR values (for every combination of allocation method) are computed once by `compute-rev-requirements` and written to a single YAML file. Each run then picks which delivery and supply method to use, via its scenario YAML. So testing is: run `compute-rev-requirements`, check the YAML, run the batch, check the outputs. @@ -197,18 +197,20 @@ The key insight: all the subclass RR values (for every combination of allocation #### Prerequisites 1. Make sure the Google Sheet has `residual_allocation_delivery` and `residual_allocation_supply` columns. NY runs 5, 6, 9, 10, 13, 14 should have `percustomer` for both. All other runs: blank. - 2. On the server, pull the latest code: - ```bash - cd /ebs/home/jpv_switch_box/rate-design-platform - git pull - ``` + +```bash +cd /ebs/home/jpv_switch_box/rate-design-platform +git pull +``` 3. If scenario YAMLs were regenerated from the sheet, verify them: - ```bash - grep -n "residual_allocation" rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml - ``` - Runs 5, 6, 9, 10, 13, 14 should show `residual_allocation_delivery: percustomer` and `residual_allocation_supply: percustomer`. + +```bash +grep -n "residual_allocation" rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml +``` + +Runs 5, 6, 9, 10, 13, 14 should show `residual_allocation_delivery: percustomer` and `residual_allocation_supply: percustomer`. #### Run the batch From e0c6b7dfa915a77ef1c5a49ea42ad00660af548f Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 14:18:02 -0400 Subject: [PATCH 23/40] Fix framing of architecture change in EPMC context doc The delivery/supply YAML split solves the combinatorial problem (needing different allocation methods for delivery vs supply), not the EPMC subtraction bug. Rewrite to make this distinction clear. Made-with: Cursor --- .../epmc_and_supply_allocation.md | 104 +++++++++--------- 1 file changed, 53 insertions(+), 51 deletions(-) diff --git a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md index 459064f8..cbc6c559 100644 --- a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md +++ b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md @@ -1,6 +1,6 @@ # EPMC Residual Allocation and the Delivery/Supply Split -This document explains what we added to the residual allocation pipeline in March 2026, why we added it, and how to test it for New York. It's written for a teammate who needs to run NY batches and verify the outputs. +This document explains what we added to the residual allocation pipeline the week of March 23rd 2026, why we added it, and how to test it for New York. It's written for a teammate who needs to run NY batches and verify the outputs. ## 1. Why: EPMC and the supply residual problem @@ -8,21 +8,21 @@ This document explains what we added to the residual allocation pipeline in Marc EPMC stands for "equi-proportional marginal cost." It's a way of splitting the revenue requirement between customer subclasses (HP vs. non-HP). -The basic idea: every customer's share of the residual is proportional to their share of total marginal costs. If HP customers cause 3% of total marginal costs, they bear 3% of the residual. Mathematically: +The basic idea: every customer's (or subclass's) share of the residual is proportional to their share of total marginal costs. If HP customers cause 3% of total marginal costs, they bear 3% of the residual. Mathematically: $$R_i = R \times \frac{EB_i}{\sum_j EB_j \times w_j}$$ where $R$ is the total residual, $EB_i$ is customer $i$'s economic burden (sum of hourly load × hourly marginal price), and $w_j$ is the sample weight. -This is equivalent to scaling all MC-based rates by a uniform constant $K = TRR / MC_{Revenue}$. Every customer's total bill is exactly $K$ times their economic burden. The markup ratio is the same for everyone. +This is equivalent to scaling all MC-based rates by a uniform constant $K = TRR / MC_{Revenue}$. Every customer's total bill is exactly $K$ times their economic burden. The markup ratio is the same for everyone. (This is why applying EPMC residual allocation to individual customers or to the subclasses as a whole gives you the same result in terms of total subclass residual.) ### Why did we add it? -The existing per-customer allocation splits the residual equally across all customers, regardless of their load. This is the "lump-sum" approach — efficient in theory (no distortion of consumption decisions), but it doesn't reflect cost causation. Utilities and regulators accustomed to embedded cost-of-service (ECOS) analysis tend to prefer cost-causation-based allocation, where the residual is assigned to customers in proportion to how much system cost they drive. +The existing per-customer allocation splits the residual equally across all customers, regardless of their load. This is the "lump-sum" approach (see (NARUC, 1992, p. 162) — efficient in theory (no distortion of consumption decisions), but it doesn't reflect cost causation. Utilities and regulators accustomed to embedded cost-of-service (ECOS) analysis tend to prefer cost-causation-based allocation, where the residual is assigned to customers in proportion to how much system cost they drive. -EPMC captures this perspective: if your load drives more marginal cost, you bear more residual. It's the standard reconciliation method used in California (CPUC) and is referenced in the BAT paper itself (Simeone et al. 2023, Appendix B, Eq. B3). +EPMC captures this perspective: if your load drives more marginal cost, you bear more residual. It's the standard reconciliation method used in California (CPUC) and is referenced in the BAT paper itself (Simeone et al. 2023, Appendix B, Eq. B3) and in NARUC's Electric Utility Cost Alloation Manual (NARUC, 1992, p. 160). -Whether cost-causation is the _right_ normative principle for allocating sunk costs is debatable — see `context/domain/bat_mc_residual/fairness_in_cost_allocation.md` for the full discussion. The short version: historical cost causation = current cost causation is an assumption that is dubious both normatively (sunk costs shouldn't affect marginal decisions) and empirically (past investment decisions don't cleanly map to current load patterns). But it is the lens that most utility regulators actually use. +Whether cost-causation is the _right_ normative principle for allocating sunk costs is debatable — see `context/domain/bat_mc_residual/fairness_in_cost_allocation.md` for the full discussion. The short version: historical cost causation = current cost causation is an assumption that is dubious both normatively (the beneficiary-pays principle points towards cost-causation-based allocation for marginal costs but away from it for residual costs), instrumentally (sunk costs shouldn't affect marginal decisions, and they are if cost-causation-allocated residual costs are recovered through volumetric charges) and empirically (past investment decisions don't cleanly map to current load patterns). But it is the lens that most utility regulators actually use. ### The supply residual problem @@ -31,25 +31,29 @@ When we implemented EPMC and started testing, we discovered something we hadn't Here's how large the residual is, as a percentage of total revenue, for both delivery and supply: | Utility | Delivery residual | Supply residual | -| ------- | :---------------: | :-------------: | -| rie | 87% | 29% | -| cenhud | 96% | 27% | -| coned | 96% | 15% | -| nimo | 98% | 28% | -| nyseg | 98% | 36% | -| or | 95% | 30% | -| psegli | 93% | 27% | -| rge | 98% | 30% | +| ------- | ----------------- | --------------- | +| rie | 87% | 29% | +| cenhud | 96% | 27% | +| coned | 96% | 15% | +| nimo | 98% | 28% | +| nyseg | 98% | 36% | +| or | 95% | 30% | +| psegli | 93% | 27% | +| rge | 98% | 30% | Delivery residual is 87–98% of delivery revenue. This is well-known — delivery costs are dominated by infrastructure that's essentially a fixed budget, and marginal delivery costs are a small fraction of the total. -Supply residual is 15–36% of supply revenue. This is less intuitive. The retail supply rate recovers more revenue than the wholesale marginal cost of supply. The gap covers things like supplier margin, hedging/procurement overhead, RECs, capacity contract premiums above the marginal clearing price, working capital, bad debt, and other costs baked into the retail supply rate. (Note: we are currently missing ancillary MCs for NY, which would shrink the supply residual somewhat.) +Supply residual is 15–36% of supply revenue. This is less intuitive. The retail supply rate recovers more revenue than the wholesale marginal cost of supply. + +We're still trying to understand how much of this "residual" might be due to simple data quality issues (inaccuracies in our retail rates [which are multiplied by EIA kwh to become the supply RR] or our marginal costs, missing marginal costs [such as ancillary services in NY, RECs in RI and NY), temporal mis-alignment between our retail rates / kwhs / marginal costs, etc.). + +Beyond that, the gap covers things like supplier margin, hedging/procurement overhead, RECs, capacity contract premiums above the marginal clearing price, working capital, bad debt, and other costs baked into the retail supply rate. **Open questions about the supply residual:** -1. Is the residual "real" (i.e., do the retail supply rates genuinely include costs beyond energy/capacity/ancillary MCs), or is it partly an artifact of our MC inputs not capturing all supply cost components? +1. Is the residual "real" (i.e., do the retail supply rates genuinely include costs beyond energy/capacity/ancillary MCs), or is it partly of the data quality or missing data issues described above? 2. If the residual is real, what costs does it collect? Are those costs variable (they scale with consumption in time $t$, like MCs) or more like fixed budgets that have to be recovered regardless of load? -3. If they're fixed budgets, the same cross-subsidization logic applies to supply as to delivery — HP customers with higher kWh are bearing a disproportionate share of a fixed cost through the flat supply rate, just like they do on delivery. And then we need to decide how to allocate the supply residual, just like we do on delivery. +3. If they're fixed budgets, the same cross-subsidization logic applies to supply as to delivery — HP customers with higher kWh are bearing a disproportionate share of a fixed cost through the flat supply rate, just like they do on delivery. And then we need to decide how to allocate the supply residual, just like we do on delivery. Why residual allocation method makes sense here? We don't have definitive answers to these questions yet. For now, the supply residual is treated as a design parameter — each run can choose how (or whether) to allocate it. @@ -57,11 +61,11 @@ We don't have definitive answers to these questions yet. For now, the supply res ### The architecture change -Previously, `compute_subclass_rr` produced a single set of subclass revenue requirements (delivery + supply combined) under each allocation method. The supply component was derived by subtracting run 1 (delivery-only) from run 2 (delivery+supply). +Previously, `compute_subclass_rr` produced a single set of subclass revenue requirements under one allocation method (per-customer), and the delivery/supply split was implicit — supply was derived by subtracting run 1 (delivery-only) from run 2 (delivery+supply). Every run got the same allocation method for both delivery and supply; there was no way to say "use EPMC for delivery but passthrough for supply." -The problem: this subtraction is only clean when the allocation weights are the same in both runs. For per-customer and volumetric allocation, they are — customer count and kWh don't change between runs. For EPMC, they're not — HP's share of economic burden is 2.67% in the delivery-only run vs. 4.83% in the combined run (because adding supply MCs changes the EB mix). The subtraction produces a "supply EPMC" number that doesn't correspond to any principled supply-only allocation. In RI, this inflated HP's supply RR by $10M, making the HP flat rate look terrible when it should have looked good. +Once we added EPMC and passthrough, we needed the ability to mix and match. RI's HP flat rate needs EPMC delivery + passthrough supply. RI's HP seasonal rate needs per-customer delivery + per-customer supply. NY might want something different again. The old single-method structure couldn't express this. -The fix: the subclass RR YAML now has **separate delivery and supply blocks**. Each run picks its delivery allocation method and supply allocation method independently. +The fix: `compute_subclass_rr` now precomputes every available allocation method for both delivery and supply, and writes them all to the YAML. Each run then picks which delivery method and which supply method to use, via `residual_allocation_delivery` and `residual_allocation_supply` in its scenario YAML. The YAML stores all the options; the run selects the combination it needs. ### The new YAML structure @@ -107,28 +111,28 @@ The parser composes the total: `total_RR[subclass] = delivery[method_d][subclass Here's what each method does, concretely, when you apply it to a subclass: -**Passthrough** leaves the cross-subsidy intact on both the MC and the residual. HP's subclass RR equals their actual bills under the default flat rate. CAIRO then calibrates the HP tariff to recover those same bills — so the HP rate ends up being the same as the default rate. No correction at all. Use this when you don't want to touch that side of the bill (e.g., supply-side for a delivery-only rate design). +**Passthrough** leaves the cross-subsidy intact on both the MC and the residual. HP's subclass RR equals their actual bills under the default flat rate. CAIRO then calibrates the HP tariff to recover those same bills — so the HP rate ends up being the same as the default rate. No correction at all. Use this when you don't want to touch that side of the bill (e.g., supply-side for a delivery-only rate design). This isn't technically a residual allocation method, since we don't actually calculate the residual and split it up somehow, but it's a hack to allow us to keep the supply (or delivery) part bill untouched by keeping the full cross-subsidy in that part of the subclass rr. -**Volumetric** fixes the MC cross-subsidy but leaves the residual allocated by kWh (same as the flat rate). HP's subclass RR = HP's economic burden + (HP kWh share × total residual). If HP's average MC per kWh is below the system average (true on supply side, where capacity peaks in summer and HP load peaks in winter), HP gets a lower subclass RR than passthrough. If HP's average MC per kWh is above average (true on delivery side in summer-peaking systems), HP gets a higher subclass RR. +**Volumetric** fixes the MC cross-subsidy but leaves the residual allocated by kWh (same as the flat rate). HP's subclass RR = HP's economic burden + (HP kWh share × total residual). If HP's average MC per kWh is below the system average (true on supply side, where capacity peaks in summer and HP load peaks in winter), HP gets a lower subclass RR than passthrough. If HP's average MC per kWh is above average (true on delivery side in summer-peaking systems), HP gets a higher subclass RR. This is not one of the residual allocation methods identified in NARUC's Electric Utility Cost Alloation Manual, because it isn't commonly applied to customer class allocation, but it is de-facto being apply to customer subclass allocation, so it's good to be able to represent it. -**EPMC** fixes the MC cross-subsidy and allocates the residual proportional to each subclass's share of total MC. This usually gives HP a lower total residual allocation than volumetric (because HP's MC share is typically less than their kWh share on supply, and the reverse on delivery). On delivery, where residual is 87–98% of costs, the allocation method matters enormously. On supply, where residual is 15–36%, the difference between EPMC and volumetric is small. +**EPMC** fixes the MC cross-subsidy and allocates the residual proportional to each subclass's share of total MC. This usually gives HP a lower total residual allocation than volumetric (because HP's MC share is typically less than their kWh share on supply, and the reverse on delivery). On delivery, where residual is 87–98% of costs, the allocation method matters enormously. On supply, where residual is 15–36%, the difference between EPMC and volumetric is small. This is known as equi-proportional residual allocation in NARUC's Electric Utility Cost Alloation Manual (NARUC, 1992, p. 160). -**Per-customer** fixes the MC cross-subsidy and allocates the residual evenly per person. This removes the volumetric distortion the most — HP customers with high kWh no longer bear a disproportionate share of the fixed-cost residual. It ends up with the lowest HP subclass cost allocation for both delivery and supply, because HP customers are a small fraction of total customer count (2% in RI). +**Per-customer** fixes the MC cross-subsidy and allocates the residual evenly per person. This removes the volumetric distortion the most — HP customers with high kWh no longer bear a disproportionate share of the fixed-cost residual. It ends up with the lowest HP subclass cost allocation for both delivery and supply, because HP customers are a small fraction of total customer count (2% in RI). This is known as "lump sum" residual allocation in NARUC's Electric Utility Cost Alloation Manual (NARUC, 1992, p. 160). ### How much the allocation method matters: delivery vs. supply The impact of the allocation method depends on how large the residual is relative to total costs. On delivery, where 87–98% of the bill is residual, the allocation method is decisive — it determines almost the entire subclass RR. On supply, where 15–36% is residual, the allocation method matters but the MC component dominates. | Utility | Delivery residual % | Supply residual % | -| ------- | :-----------------: | :---------------: | -| rie | 87% | 29% | -| cenhud | 96% | 27% | -| coned | 96% | 15% | -| nimo | 98% | 28% | -| nyseg | 98% | 36% | -| or | 95% | 30% | -| psegli | 93% | 27% | -| rge | 98% | 30% | +| ------- | ------------------- | ----------------- | +| rie | 87% | 29% | +| cenhud | 96% | 27% | +| coned | 96% | 15% | +| nimo | 98% | 28% | +| nyseg | 98% | 36% | +| or | 95% | 30% | +| psegli | 93% | 27% | +| rge | 98% | 30% | This is why we didn't bother with volumetric on the delivery side until now — when 98% of the cost is residual, the MC correction is almost irrelevant; what matters is how you allocate the residual. On the supply side, the MC correction is more meaningful (it's 64–85% of the bill), and the residual allocation method matters less. @@ -181,14 +185,10 @@ The previous section explains the theory. This section tells you exactly how to ### What was implemented (high level) 1. **CAIRO monkey-patches** (`utils/mid/patches.py`): added EPMC residual allocation, disabled broken peak allocation, added BAT_epmc to the cross-subsidization CSV. - -2. **`compute_subclass_rr.py`**: computes all delivery and supply allocation methods in a single pass and writes the new YAML structure with separate delivery/supply blocks. - -3. **`scenario_config.py` + `run_scenario.py`**: the parser reads `residual_allocation_delivery` and `residual_allocation_supply` from the scenario YAML, composes the total RR from the delivery + supply blocks. - -4. **`build_master_bat.py`**: detects available BAT metrics at runtime — gracefully handles missing BAT_peak and new BAT_epmc columns. - -5. **`create_scenario_yamls.py`**: reads `residual_allocation_delivery` and `residual_allocation_supply` columns from the Google Sheet. +2. `**compute_subclass_rr.py`**: computes all delivery and supply allocation methods in a single pass and writes the new YAML structure with separate delivery/supply blocks. +3. `**scenario_config.py` + `run_scenario.py**`: the parser reads `residual_allocation_delivery` and `residual_allocation_supply` from the scenario YAML, composes the total RR from the delivery + supply blocks. +4. `**build_master_bat.py**`: detects available BAT metrics at runtime — gracefully handles missing BAT_peak and new BAT_epmc columns. +5. `**create_scenario_yamls.py**`: reads `residual_allocation_delivery` and `residual_allocation_supply` columns from the Google Sheet. The key insight: all the subclass RR values (for every combination of allocation method) are computed once by `compute-rev-requirements` and written to a single YAML file. Each run then picks which delivery and supply method to use, via its scenario YAML. So testing is: run `compute-rev-requirements`, check the YAML, run the batch, check the outputs. @@ -197,18 +197,20 @@ The key insight: all the subclass RR values (for every combination of allocation #### Prerequisites 1. Make sure the Google Sheet has `residual_allocation_delivery` and `residual_allocation_supply` columns. NY runs 5, 6, 9, 10, 13, 14 should have `percustomer` for both. All other runs: blank. - 2. On the server, pull the latest code: - ```bash - cd /ebs/home/jpv_switch_box/rate-design-platform - git pull - ``` + +```bash +cd /ebs/home/jpv_switch_box/rate-design-platform +git pull +``` 3. If scenario YAMLs were regenerated from the sheet, verify them: - ```bash - grep -n "residual_allocation" rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml - ``` - Runs 5, 6, 9, 10, 13, 14 should show `residual_allocation_delivery: percustomer` and `residual_allocation_supply: percustomer`. + +```bash +grep -n "residual_allocation" rate_design/hp_rates/ny/config/scenarios/scenarios_cenhud.yaml +``` + +Runs 5, 6, 9, 10, 13, 14 should show `residual_allocation_delivery: percustomer` and `residual_allocation_supply: percustomer`. #### Run the batch From 71cb4deffc8e71bdd828fa185e1dac57680f8287 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 14:25:32 -0400 Subject: [PATCH 24/40] [context] Add NRRI paper on TOU design methods --- context/README.md | 9 +- ...ciently_rate_design_options_and_methods.md | 1000 +++++++++++++++++ 2 files changed, 1005 insertions(+), 4 deletions(-) create mode 100644 context/sources/nrri_how_to_induce_consumers_to_consume_energy_efficiently_rate_design_options_and_methods.md diff --git a/context/README.md b/context/README.md index 9c0ea314..d3afcf63 100644 --- a/context/README.md +++ b/context/README.md @@ -178,10 +178,11 @@ Academic papers (e.g. Bill Alignment Test, Faruqui). Extracted from PDFs. ### sources/ (top-level) -| File | Use when working on … | -| ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| nyiso_gold_book_2025.md | NYISO 2025 Load & Capacity Data Report ("Gold Book"), all tables machine-extracted from PDF. The authoritative source for NY system-level data: NYCA and zonal (A–K) baseline energy and peak demand forecasts through 2055 (summer and winter, with lower/higher demand scenarios), component-level forecast breakdowns (EE, solar PV BTM, EVs, building electrification, energy storage, large loads, demand response), 90th/10th/99th percentile weather-driven peak forecasts, G-to-J locality peaks, existing generator capability by zone and type, proposed generation changes, capacity schedule, transmission facilities. We use it for utility system peaks (NiMo zones A–F, PSEG-LI zone K, ConEd zone J, etc.) when computing MCOS dilution, for electrification scenario assumptions, and as the reference for NY load growth trajectories in rate design. | -| lipa_2025_2026_budget_one_pager.md | LIPA 2026 vs 2025 budget fact sheet: residential bill impact, operating/capital/revenue budgets, funding sources, regionally comparable rates | +| File | Use when working on … | +| --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| nrri_how_to_induce_consumers_to_consume_energy_efficiently_rate_design_options_and_methods.md | NRRI (Pollock & Shumilkina, 2010) guide to efficiency-inducing rates: inclining block rates, seasonal rates, TOU rates, critical peak pricing, real-time pricing — design goals, benefits, costs, programmatic decisions, redistributive consequences, renewable energy implications, EIR overlap. Part Two is a step-by-step TOU rate design manual: data requirements, season selection (correlation/distance), cluster analysis for hour grouping, rate setting (MC-based with revenue-requirement scaling), elasticity-based load/revenue estimation (own-price and cross-price matrices from Faruqui & George 2002), sensitivity analysis, finalization. Appendices include a data request template, Utility X data, and elasticity literature review (-0.12 to -0.35 short-run range). | +| nyiso_gold_book_2025.md | NYISO 2025 Load & Capacity Data Report ("Gold Book"), all tables machine-extracted from PDF. The authoritative source for NY system-level data: NYCA and zonal (A–K) baseline energy and peak demand forecasts through 2055 (summer and winter, with lower/higher demand scenarios), component-level forecast breakdowns (EE, solar PV BTM, EVs, building electrification, energy storage, large loads, demand response), 90th/10th/99th percentile weather-driven peak forecasts, G-to-J locality peaks, existing generator capability by zone and type, proposed generation changes, capacity schedule, transmission facilities. We use it for utility system peaks (NiMo zones A–F, PSEG-LI zone K, ConEd zone J, etc.) when computing MCOS dilution, for electrification scenario assumptions, and as the reference for NY load growth trajectories in rate design. | +| lipa_2025_2026_budget_one_pager.md | LIPA 2026 vs 2025 budget fact sheet: residential bill impact, operating/capital/revenue budgets, funding sources, regionally comparable rates | ### sources/mcos/ diff --git a/context/sources/nrri_how_to_induce_consumers_to_consume_energy_efficiently_rate_design_options_and_methods.md b/context/sources/nrri_how_to_induce_consumers_to_consume_energy_efficiently_rate_design_options_and_methods.md new file mode 100644 index 00000000..92a96709 --- /dev/null +++ b/context/sources/nrri_how_to_induce_consumers_to_consume_energy_efficiently_rate_design_options_and_methods.md @@ -0,0 +1,1000 @@ +# How to Induce Customers to Consume Energy Efficiently: Rate Design Options and Methods + +**Source**: nrri_how_to_induce_consumers_to_consume_energy_efficiently_rate_design_options_and_methods.pdf +**Pages**: 64 +**Date**: January 2010 +**Author(s)**: Adam Pollock and Evgenia Shumilkina +**Organization**: National Regulatory Research Institute (NRRI) +**Report Number**: 10–03 + +_Note: Minor typographical errors in the source (e.g. "Rsate" for "Rate", "Ferbruary" for "February", "carious" for "various", "marked" for "markedly") have been corrected for readability. Substantive corrections are annotated inline._ + +--- + +## Acknowledgments + +The authors would like to thank David Iraola, who researched several of the topics addressed herein, and David Boonin for his substantive input. Washington, D.C. PUC Commissioner, Rick Morgan, also contributed to this report. We would also like to thank Leah Goodwin and Scott Hempling for their editing contributions. This report builds on the work of Ahmad Faruqui, who has written numerous papers and articles on using rate designs to promote efficient consumer behavior. + +**Online Access**: This paper can be found online at [NRRI](www.nrri.org/pubs/electricity/NRRI_inducing_energy_efficiency_jan10-03.pdf). + +--- + +## Executive Summary + +Utilities and their regulators view a different world than the one their customers perceive. In the world seen by most electricity customers, electricity costs the same amount regardless of how much is used or when. In contrast, utilities and their regulators see a world where electricity costs vary by the hour, infrastructure investments loom, and new or upcoming legislation requires reductions in electricity consumption or carbon emissions. "Efficiency-inducing rates (EIRs)"—defined here as rates that vary by time, condition, or customer behavior in order to induce efficient electricity consumption—bridge this perception gap. By aligning rates with electricity costs, they encourage customers to use electricity when it is least costly and lower their overall consumption. + +This report seeks to empower regulators to evaluate and propose EIRs or effectively scrutinize utility proposals. It examines EIR options including inclining block rates, seasonal rates, time-of-use rates, critical peak pricing programs, and real-time pricing. On October 27, 2009 the federal government provided $3.4 billion of grants to 100 "smart grid" projects. Most smart grid projects include deployment of advanced meters, which facilitate certain EIRs. Regulators should evaluate rate options to maximize smart grid benefits. + +Each EIR features unique advantages, disadvantages, and design choices. Certain decisions, such as who is eligible and whether rates are mandatory, apply to all EIRs. Additionally, some EIRs can operate in conjunction with others. Because regulators do not make rate design decisions in a vacuum, this report also explains how various EIRs affect different customer classes and how they complement or detract from distributed renewable energy development. + +This report also guides regulators step-by-step through the process of crafting seasonal and time-of-use rates, which will also enable scrutiny of utility rate proposals. For instance, it describes how to select the optimal number of seasons and time periods, how much rates should vary between periods, and how to estimate customer behavioral changes in response to new EIRs. Many of the discussed processes and principles for fashioning time-of-use rates also apply to designing other EIRs. + +--- + +# Part One: Efficiency-Inducing Rates: Evaluation and Implementation + +## Table of Contents (Part One) + +- I. Introduction to Efficiency-Inducing Rates: What Are Their Purposes, Their Benefits, and Their Risks? + - A. Rate design overview + - B. Inclining block rates + - C. Seasonal rates + - D. Time-of-use rates + - E. Critical peak pricing + - F. Real-time pricing +- II. Common Implementation Considerations and Decisions + - A. To whom should rates apply? + - B. Should rates be mandatory, opt-in, or opt-out? + - C. How should regulators estimate changes in consumption and revenues after the introduction of EIRs? + - D. How should regulators address utility risk changes from EIRs? + - E. How should utilities educate customers about EIRs? +- III. Efficiency-Inducing Rate Overlap + - A. Combining real-time pricing and critical peak pricing, time-of-use, or seasonal rates + - B. Combining critical peak pricing, time-of-use, and seasonal rates + - C. Combining inclining block rates and all other EIRs + - D. Combining critical peak pricing and demand response programs +- IV. Secondary Effects of Efficiency-Inducing Rates + - A. Redistributive consequences + - B. Renewable energy implications of EIRs + +--- + +## Introduction + +Faced with increasing generation costs and environmental goals or mandates, regulators are examining methods to induce customers to consume electricity more efficiently by reducing both their aggregate and peak consumption. Some utilities also face capacity margin shortfalls, which could degrade reliability without new generation or transmission investments. "Efficiency Inducing Rates (EIRs)" address these challenges by encouraging customers to reduce peak demand and overall consumption. Thus regulators are using economic levers to optimize customer behavior.[^1] + +[^1]: For more discussion on encouraging but not mandating optimal behavior, please see _Nudge_ by Richard Thaler and Cass Sunstein and see Scott Hempling's essay "Decisional Defaults: Does Regulation Have Them Backwards?" at [NRRI](http://www.nrri2.org/index.php?option=com_content&task=view&id=179&Itemid=38). + +We define EIRs as rates that vary by time, condition, or customer behavior in order to induce efficient electricity consumption.[^2] They include inverted block rates, seasonal rates, time-of-use (TOU) rates, critical peak pricing (CPP), and real-time pricing (RTP). + +[^2]: EIRs include "dynamic rates" such as real-time and critical peak pricing rates as well as other non-flat rate designs. + +This paper informs regulators about EIR options and their consequences. It will assist them in either designing rates or evaluating utility proposals. It provides examples and discusses what data and methodologies regulators should use to design or evaluate rates. Such rates maximize the value of smart grid technologies, including those funded through federal grants. + +Part One, Section I discusses rate design goals and efficiency implications. It also describes each EIR, including potential benefits, costs or concerns, and unique design decisions. Section II explores common design decisions, such as whether rates are mandatory or optional, that commissions confront when designing all EIRs. Section III examines how different EIRs could overlap with each other. Finally, Section IV describes how EIRs affect different customer groups and how they enhance or diminish the economic value of distributed renewable energy projects. + +Part Two explains how to craft seasonal/TOU rates. Certain principles and practices addressed in Part Two, such as the use of estimates of elasticities of demand, also apply to evaluating other EIRs. + +--- + +### Table 1. Efficiency-Inducing Rate Options + +| Rate Design | Description | +| --------------------- | --------------------------------------------------------------------------------------------------------------------- | +| Inclining block rates | Rates that increase at higher levels of electricity consumption | +| Seasonal rates | Rates that vary by season | +| Time-of-use rates | Rates that vary by time of day and day of the week | +| Critical peak pricing | Programs allowing the utility to dramatically increase rates on short notice a predetermined number of times per year | +| Real-time pricing | Rates that adjust in real-time based on wholesale electricity costs | + +--- + +## I. Introduction to Efficiency-Inducing Rates: What Are Their Purposes, Their Benefits, and Their Risks? + +### A. Rate design overview + +#### 1. Rate design goals + +The two primary goals of rate design are to (a) provide rates that lead to utility revenues matching the revenue requirement and (b) allocate both fixed and variable costs to responsible customers. Effective rate designs also: + +1. Minimize complexity, recognizing differing degrees of customer sophistication; +2. Maximize cost predictability and stability for customers and revenue certainty for utilities; +3. Incent utilities to minimize costs; and +4. Incent customers to consume electricity efficiently by minimizing peak demand and/or total consumption. + +Rate designs in some jurisdictions also seek to: + +1. Improve electricity affordability for poor or vulnerable populations ("lifeline rates"); +2. Promote economic development, often by providing lower rates for industrial customers; and +3. Maximize customer choice. + +When evaluating EIRs, regulators should examine their consequences for the two primary rate design goals and for the secondary rate design goals. Regulators must determine whether improvements in the effectiveness of rates in some regards due to changes in rate design outweigh corresponding reductions in other forms of rate design effectiveness. + +#### 2. Rate design and efficiency + +Traditional rate designs featuring a combination of fixed charges and variable charges based on constant per-kWh rates do not reflect the actual costs of electricity. Such costs differ based on the time-of-day, season, transmission and generation availability, and other factors.[^3] Traditional rate designs do not reflect these variations beyond fuel cost adjustment charges, which at best reflect only seasonal changes in fuel costs and are often subject to substantial regulatory lag. Thus, while the utility's cost of electricity in peak summer hours could be $0.50 per kWh, the energy component of the customer's rate could be $0.10 per kWh, the same as it is when electricity cost the utility $0.04 per kWh during the off-peak hours of shoulder months. + +[^3]: Peak real-time spot market electricity costs can exceed average costs by more than tenfold. See [PJM 2008 State of the Markets Report](http://www.monitoringanalytics.com/reports/PJM_State_of_the_Market/2008/2008-som-pjm-volume2-sec2.pdf). + +Further, some rate designs discourage energy efficiency. Certain utilities offer rates that decrease with higher consumption. Such rates often reflect the variable (energy) rate component including fixed costs, causing average electricity costs to exceed marginal electricity costs. + +By contrast, EIRs align customer behavior with the actual cost of electricity. Except for inclining block rates, which encourage customers to minimize overall rather than peak consumption, EIRs encourage efficient consumption. Savings from energy efficiency come in two primary forms: (1) reduced electricity and capacity costs and (2) delayed or avoided infrastructure investments. + +Regional transmission organizations (RTOs) and utilities dispatch generation in order of increasing costs, subject to reliability constraints. In most organized markets, the last generator dispatched sets the price for all electricity sales.[^4] Consequently, dispatch of expensive generators increases the average cost paid for electricity far more than the average costs of producing electricity. Conversely, reducing peak demand, and thus the use of expensive units, provides large savings by reducing payments to all other units. + +[^4]: Most regional transmission organizations operate wholesale electricity markets that feature a single market-clearing price. + +[DIAGRAM DESCRIPTION: PJM Supply Curve / Cost vs. Demand] + +A graph from the 2008 State of the Markets Report for PJM illustrating that the cost of electricity in PJM increases steeply after regional demand exceeds approximately 160,000 MW. The curve shows marginal cost on the vertical axis and regional demand (MW) on the horizontal axis. The supply curve is relatively flat at low demand levels, then rises sharply as demand approaches system capacity, demonstrating that even small reductions in peak demand during those peak hours could substantially lower aggregate costs. + +[→ See original PDF page 4 for visual rendering] + +Traditionally regulated utilities receive smaller short-term savings from peak demand reductions than do utilities who acquire electricity through organized markets. Customers of traditionally regulated utilities pay the same depreciation and returns on utility-owned generators, regardless of energy costs. Such utilities also pass through total energy costs to customers, so reductions in peak demand do not affect the costs paid for all wholesale electricity. + +Aside from reducing the dispatch of the most expensive units, peak demand reductions allow utilities to defer transmission and distribution upgrades as well as new generation capacity because systems must accommodate peak demand. Long-run marginal cost calculations consider the capital costs of serving additional load. Avoided infrastructure savings from EIRs, while potentially large, would not be immediate because the avoided investments would not enter rate base for some time and would depreciate over a long period. + +In organized markets, long-run savings come from avoided transmission and distribution investments and lower capacity payments. The savings from avoiding infrastructure development are larger for customers of traditionally regulated utilities. There, the savings come from avoided transmission or distribution and avoided generators (peaking or base load), which would otherwise increase utility rate bases and thus rates.[^5] + +[^5]: RTOs through planning and price signals, elicit the development of transmission and distribution infrastructure as well as new generation capacity. However, certain writers, state commissions, and legislatures, such as Maryland's, have found that RTOs do not send accurate price signals to prospective builders of additional capacity. + +EIRs optimize electricity consumption by encouraging load shifting and reductions in total consumption because rates reflect actual electricity costs. Some customers can shift their consumption to hours or even seasons that are less expensive. For instance, customers with TOU rates could wash clothes at night instead of during the day. Certain EIRs also can incent customers to minimize overall electricity consumption. For instance, inclining block rates render electricity more expensive at higher consumption levels, encouraging customers to keep their consumption below block thresholds. + +### B. Inclining block rates + +#### 1. Description + +Inclining block rates, sometimes called inverted block rates or baseline rates, feature one per-kWh rate for the first monthly kWh block of consumption and higher rates for each subsequent block. These rates best suit residential customers. Their consumption is more predictable than commercial, industrial, or agricultural customers, whose size and electricity needs vary markedly, inhibiting identifying optimal block sizes. Unlike other EIRs, inclining block rates are usually mandatory rather than optional. + +Inclining block rates can motivate customers to reduce consumption to attainable levels. Incentives to improve energy efficiency increase for heavy users of electricity but diminish for customers who already consume little electricity. Inclining block rates do not encourage load shifting because they disregard consumption timing. + +[DIAGRAM DESCRIPTION: PNM Inclining Block Rate Schedule] + +An example rate schedule from PNM featuring three rate blocks coupled with seasonal rates.[^6] The schedule shows per-kWh energy charges for summer and non-summer seasons, with rates increasing in each subsequent consumption block. The cutoff between the second and third tiers is 500 kWh; the average PNM customer consumes 600 kWh per month, so most customers' consumption reaches into the highest block. The structure encourages most customers to reduce consumption by approximately 100 kWh — a meaningful but attainable goal. Specific per-kWh rate values for each block and season are visible only in the PDF image. + +[→ See original PDF page 6 for rate values] + +[^6]: [PNM Rate Schedule 1A](http://www.pnm.com/regulatory/pdf_electricity/schedule_1_a.pdf) + +#### 2. Benefits + +Inclining block rates induce energy efficiency measures such as efficient heating and air conditioning systems and improved insulation. They can also encourage distributed generation, such as solar panels. Such measures reduce overall energy consumption. Inclining block rates help utilities meet consumption reduction obligations or carbon emissions legislation. According to one study, inclining block rates provide energy consumption savings of six percent for the first few years and more over the long run.[^7] Unlike critical peak pricing and real-time pricing, inclining block rates do not necessitate any additional infrastructure, such as advanced metering infrastructure (AMI). + +[^7]: Faruqui, Ahmad, "Inclining Towards Efficiency," _Public Utilities Fortnightly_, August 2008, page 25. + +#### 3. Costs and concerns + +Poor families with large but old homes could see higher bills under inclining block rates but lack the ability to respond to the price signals. Regulators should examine the availability of federal, state, and utility low-income weatherization programs, which can mitigate this problem. + +For certain customers, inclining block rates reduce energy efficiency incentives. For instance, customers whose monthly consumption already falls within the lower block(s) would see lower electric bill savings from energy efficiency measures or distributed generation under inclining block rates than they would under traditional rates. + +Inclining block rates could encourage certain customers to switch their water and heating from electricity to natural gas in order to avoid consumption in costlier blocks. This practice would not reduce overall energy consumption; rather, it would just encourage use of another energy source. Thus, this rate design could favor gas-powered appliances or heating systems, allowing customers to reduce utility bills without becoming more energy-efficient—at the expense of other customers. + +Rates that reduce overall energy consumption diminish utility sales and thus revenues. The level of reduced consumption is uncertain, increasing the risk of revenues falling below the revenue requirement established in rate cases. Such uncertainty could degrade utility credit ratings and their ability to access capital. Inclining block rates also increase rate complexity for customers. + +#### 4. Programmatic decisions + +**How many blocks?** Most inclining block rates feature two to five rate blocks.[^8] This range presents customers with identifiable consumption goals. One suggested structure includes four blocks. The lowest block's rate aligns with the embedded costs of the system, the second block's rate reflects the average system costs, and the top two blocks' rates reflect the utility's long-run marginal costs of service.[^9] + +[^8]: Pacific Gas and Electric Company features five tiers for its inclining block rates. [PG&E Baseline](http://www.pge.com/myhome/customerservice/financialassistance/medicalbaseline/understand/). Puget Sound Energy features two blocks. [PSE Electric Prices](http://www.pse.com/SiteCollectionDocuments/rates/summ_elec_prices_2009_04_01.pdf). + +[^9]: [Utah PSC Residential Usage of Electricity](http://www.psc.state.ut.us/utilities/electric/07docs/0703593/61949ExhibitD.ppt#273,4,RESIDENTIAL USAGE OF ELECTRICITY) + +**What should be the kWh cutoff between blocks?** Inclining block rates should encourage most customers to reduce their electricity consumption to attainable levels. If most customers would see lower peak rates under inclining block rates without changing their consumption, the rates would not encourage energy efficiency. The summer PNM rate example above features a cutoff between the second and third tiers of 500 kWh. The average PNM customer consumes 600 kWh per month.[^10] Thus, rates encourage most customers to reduce consumption by 100 kWh, a meaningful but attainable goal. In California, the Public Utilities code requires that baseline quantities (the lowest tier) fall between 50 and 60 percent of average use for basic-electric customers in both the summer and winter and for all-electric and gas customers in the summer. The PU code also requires that, in the winter, baseline quantities fall between 60 and 70 percent of average use for all-electric and gas customers.[^11] + +[^10]: See PNM press release: [PNM 2009 Rates Approved](http://www.pnm.com/news/2009/0528_rates_approved.htm) + +[^11]: [PG&E Medical Baseline](http://www.pge.com/myhome/customerservice/financialassistance/medicalbaseline/understand/) + +**How much should rates differ between blocks?** Large rate differences between blocks increase the energy efficiency incentives for high-consuming customers. Conversely, large differences reduce the energy efficiency incentives for low-consuming customers. Large differences could also cause certain customers to pay far more than the actual cost of their electricity, running counter to the second primary goal of rate design shown in I(A): "Allocate both fixed and variable costs to responsible customers." + +Inclining block rates should reflect cost causality, specifically long-run marginal costs. In other words, high-use customers who place greater long-run costs on the system should pay accordingly. Such customers necessitate additional transmission, distribution, and generation investments. Such customers' rates should reflect the long-run marginal costs of electricity use, even if the short-run marginal cost of supplying customers decreases as consumption goes up. + +**Should inclining block rates consider household characteristics?** Larger families are likely to consume more electricity than smaller ones. Block sizes could differ based on customer household size because reasonable consumption for a single person is different than for a large family. Such a policy would increase administrative cost, however. As an example of differentiating rates based on customer characteristics, Pacific Gas and Electric's baseline rates feature an exception for persons with medical conditions requiring equipment or special heating/cooling needs.[^12] PG&E's baselines and thus blocks also differ within utilities by region to account for weather differences.[^13] + +[^12]: [PG&E Medical Baseline](http://www.pge.com/myhome/customerservice/financialassistance/medicalbaseline/) + +[^13]: [PG&E Tariff Schedule E-1](http://www.pge.com/tariffs/tm2/pdf/ELEC_SCHEDS_E-1.pdf) + +### C. Seasonal rates + +#### 1. Characteristics + +Seasonal rates reflect the high average cost of electricity during the winter or summer. They typically feature two seasons and often combine with TOU rates. Seasonal rates suit regions with distinct seasonal variations in electricity demand and costs. + +[DIAGRAM DESCRIPTION: Average Weekly Electricity Spot Prices at Four Eastern Trading Hubs] + +A graph from the 2008 FERC State of the Markets Report showing the average weekly electricity spot prices at four Eastern trading hubs over the course of a year.[^14] Each hub features a summer peak and two also feature winter peaks. The vertical axis shows price ($/MWh) and the horizontal axis shows weeks of the year. The graph demonstrates clear seasonal variation in wholesale electricity costs, with summer peaks being the most pronounced. + +[→ See original PDF page 9 for visual rendering] + +[^14]: [FERC State of the Markets](http://www.ferc.gov/market-oversight/st-mkt-ovr/st-mkt-ovr.asp) + +[DIAGRAM DESCRIPTION: Public Service Company of Colorado Seasonal Rate Schedule] + +An example of seasonal rates from Public Service Company of Colorado (Xcel Energy) applying to commercial customers.[^15] The schedule shows different per-kWh energy charges for summer and non-summer periods, with higher rates during the summer peak season to reflect higher wholesale electricity costs. Specific per-kWh rate values for each season are visible only in the PDF image. + +[→ See original PDF page 9 for rate values] + +[^15]: [Xcel Energy PSCO Electric Tariff](http://www.xcelenergy.com/SiteCollectionDocuments/docs/psco_elec_entire_tariff.pdf) + +#### 2. Benefits + +In response to seasonal rates, certain customers, such as large industrials, can shift some of their electricity consumption to off-peak months. Such shifts reduce utility peak demand and thus overall system costs by reducing dispatch of the most expensive generators and forestalling system upgrades. + +A majority of customers cannot shift their load between seasons. Instead, seasonal rates encourage consuming less electricity and pursuing energy efficiency measures during the peak seasons. For instance, higher summer rates incent investment in high-efficiency air conditioners, lowering overall consumption and carbon emissions. Such resulting efficiency measures to some extent decrease peak demand as well. + +Although more complicated than flat rates, seasonal rates are predictable, unlike critical peak pricing or real-time rates, such that unsophisticated (e.g., residential) customers do not need to follow rates. Finally, seasonal rates do not necessitate infrastructure expenditures such as advanced meters. + +#### 3. Costs and concerns + +Customers who are unable to readily shift their load to off-peak seasons or reduce their overall consumption could face higher overall electricity bills. Seasonal rates also increase rate complexity. Additionally, as discussed in section I.B.3, rate designs that affect consumer behavior increase utility risk of revenue under-recovery. + +#### 4. Programmatic decisions + +Part Two provides practical guidelines for how to approach these decisions. + +**Which seasons should be peak and off-peak?** Certain regions, like the Southwest, feature prominent summer peaks in both demand and wholesale electricity costs. Others, such as the Midwest, include summer and winter peaks. Certain states, such as Hawaii, feature low variation between seasons. To determine whether peak rates should be in the summer, winter, both, or neither, examine utilities' average monthly peak demand and average electricity costs. + +**What should be the duration of peak seasonal periods?** Determine the timing and duration of peak periods based on utilities' average monthly peak demand and electricity costs. + +**How large should the discrepancy be between rates for peak and off-peak seasons?** Large differences between seasonal rates increase incentives to shift or reduce consumption. They also reduce incentives to conserve electricity in off-peak seasons. Regulators should base rate differentials on variations in average wholesale electricity costs. + +### D. Time-of-use rates + +#### 1. Description + +Time-of-use rates feature different rates at different hours of the day and days of the week. TOU rates include peak and off-peak rates and, in some cases, mid-peak rates for shoulder hours. Weekends and holidays are usually off-peak. TOU rates often combine with seasonal rates. + +[DIAGRAM DESCRIPTION: PJM Hourly Locational Marginal Price Variation] + +A graph from the 2008 State of the Markets Report for PJM illustrating the variation between average locational marginal prices during different hours.[^16] The graph shows average LMP ($/MWh) on the vertical axis and hour of the day (1–24) on the horizontal axis, with prices peaking during midday and afternoon hours and reaching their lowest during overnight hours. Individual seasons or days of the week can feature larger variations. + +[→ See original PDF page 11 for visual rendering] + +[^16]: Locational marginal price (LMP): Used in certain organized markets, the LMP is the cost to serve the next MW of load at a specific location, using the lowest production cost of all available generation, while observing all transmission limits. + +[DIAGRAM DESCRIPTION: Connecticut Light and Power Residential TOU Rate Schedule] + +An example of residential TOU rates from the Connecticut Light and Power Company.[^17] The schedule shows different per-kWh energy charges for on-peak and off-peak periods, with higher rates during peak weekday hours and lower rates during nights, weekends, and holidays. Specific per-kWh rate values and period definitions are visible only in the PDF image. + +[→ See original PDF page 11 for rate values] + +[^17]: [CL&P Rate 37](http://nuwnotes1.nu.com/apps/clp/clpwebcontent.nsf/AR/rate37/$File/rate37.pdf) + +#### 2. Benefits + +Unlike seasonal rates, TOU rates enable most customers to shift consumption away from peak periods. For instance, customers can wash clothes and dishes at night. Such shifts reduce peak demand and thus overall electricity costs and avoid infrastructure investments. + +Where customers cannot shift their consumption, TOU rates can encourage lower total consumption and energy efficiency measures. For instance, TOU rates incent customers to raise their thermostats during peak, summer hours, but few customers respond by using their air conditioners more during off-peak, night hours.[^18] + +[^18]: TOU rates lower rates during off-peak periods. It is possible that the lower rates during these periods will reduce energy efficiency incentives such that aggregate consumption does not fall compared to flat rates. + +TOU rates are predictable, unlike critical peak pricing or real-time rates, such that unsophisticated customers can participate without following price signals. + +#### 3. Costs and concerns + +TOU rates can increase electricity costs for certain customers, such as retail stores, whose consumption falls heavily during peak periods but cannot be readily reduced or shifted. + +TOU rates can require the installation of new meters capable of recording when customers consume electricity and bill them accordingly. TOU rates also feature greater complexity than do flat rates. Finally, as discussed in section I.B.3, rate designs that affect consumer behavior increase utility revenue uncertainty. + +#### 4. Programmatic decisions + +Regulators must address the following programmatic discussions, which Part Two describes in more detail and provides practical solutions for. + +**How many tiers should TOU rates include?** Most TOU rates include two or three rate tiers. TOU rates with mid-peak rates often better align the costs of electricity with retail rates than do two-tier rates. Such precision comes at the cost of added complexity, however. Regulators should examine fluctuations in average hourly wholesale electric costs. Where average wholesale costs bifurcate neatly between peak and off-peak periods, mid-peak rates are unnecessary. Mid-peak rates could also cover weekends and holidays, which often feature usage lower than normal weekdays but higher than nights. + +**What should be the peak, mid-peak, and off-peak hours?** TOU rates should correlate to average hourly electricity costs. Regulators should examine average electricity costs for different hours and days over multiple years to control for weather fluctuations. + +**How much should peak, mid-peak, and off-peak TOU rates differ?** Larger rate differences lead to high incentives to shift or reduce consumption. They also reduce incentives to conserve during off-peak periods. Further, large differentials increase bills for customers whose consumption takes place heavily on-peak and who cannot readily shift or reduce consumption. Regulators should base TOU rates on the differences in the wholesale electricity costs during different times of day or days of the week to align rate signals with costs. + +### E. Critical peak pricing + +#### 1. Description + +Critical peak pricing (sometimes called "dynamic peak pricing") programs allow the utility to increase rates on short notice (often one day) for a defined period (often several hours) a certain number of times a year (e.g., 10). In exchange, participating customers receive rate discounts during other periods. Most utilities enjoy wide discretion to call critical peak events based on economic or reliability considerations. CPP programs often combine with TOU rates. + +CPP programs differ from certain RTO economic demand response programs, which credit customers for reducing demand at certain times but do not change rates. Also, unlike certain demand response programs in which the utility can control customer loads, CPP participants can continue consuming electricity, though at high rates. + +Peak time rebate (PTR) programs, though not technically rates, provide an alternative to CPP programs with similar costs and benefits. PTR programs provide a rebate against customers' electricity bills if they reduce consumption in critical events compared to their baseline consumption. They do not change their electricity rates. Unlike some CPP programs, customer bills cannot increase under PTR programs compared to flat rates if they do not reduce consumption. This lack of customer risk could improve participation rates for peak time rebate programs compared to CPP programs. + +Baltimore Gas and Electric Company has conducted pilots for CPP and PTR programs and Xcel Energy proposed to offer both programs for certain Colorado customers.[^19][^20] Baltimore Gas and Electric found that customer satisfaction was higher for PTRs than CPP despite similar reductions in peak demand.[^21] Customers found peak time rebates easier to understand and most supported PTR as the default rates and not CPP rates. + +[^19]: [BGE Smart Energy](www.bgesmartenergy.com/media/ses/SESP%20Fact%20Sheet.doc) + +[^20]: [Daily Camera - Boulder County News](http://www.dailycamera.com/boulder-county-news/ci_13732272) + +[^21]: [BGE Demand Response Town Meeting Presentation](www.demandresponsetownmeeting.com/.../Cheryl%20Hindes%20BGE_SESSION%20A,%207.14.09.ppt) + +The discount from CPP programs tends to come from the energy component of rates during other hours, reducing energy efficiency incentives. Contrastingly, the customer benefits from PTR programs do not lower rates at other times, thus maintaining energy efficiency incentives during other hours. + +[DIAGRAM DESCRIPTION: Portland General Electric Residential CPP Pilot Rate Schedule] + +An example of Critical Peak Pricing, combined with TOU and seasonal rates, from Portland General Electric Company's residential CPP pilot.[^22] The schedule shows multiple rate tiers layered together: a standard off-peak rate, a standard on-peak rate, and a significantly higher critical peak rate that applies during designated CPP events. Rates also vary by season (summer vs. non-summer). Portland General Electric contacts customers the day before a CPP event. Specific per-kWh rate values for each tier and season are visible only in the PDF image. + +[→ See original PDF page 14 for rate values] + +[^22]: [Portland General Electric Schedule 012](http://www.portlandgeneral.com/our_company/corporate_info/regulatory_documents/pdfs/schedules/Sched_012.pdf) + +#### 2. Benefits + +By shaving even a small percentage off demand during critical hours, utilities can markedly lower electricity costs, particularly in organized markets, and avoid building new infrastructure. In some places, utilities can also bid CPP "capacity" into wholesale markets and use the revenues to offset the cost of service. + +CPP and PTR programs also enable utilities to preserve reliability during emergencies. They impose relatively low costs on non-participants because of the relatively small discounts or rebates. + +CPP or peak time rebate programs enable utilities lacking fuel cost adjustment mechanisms or decoupling mechanisms to reduce their exposure and thus risk from the highest wholesale electricity costs.[^23] They do so by initiating CPP events and correlating rates with peak energy prices. The resulting lower risk reduces utilities' need to (a) internalize risk, (b) acquire physical hedges like generators or long term power purchase agreements, or (c) purchase financial hedges. + +[^23]: Many utilities enjoy regulatory mechanisms that allow them to pass fuel or energy costs to their consumers. These mechanisms usually contain true-up provisions that match rates to these costs, although often with a lag. + +#### 3. Costs and concerns + +CPP and PTR programs require AMI infrastructure. These programs can require high customer sophistication, since they must quickly respond to CPP notifications. They also increase rate complexity and require customers to adjust their consumption on short notice. Programs that feature automated controls to cycle appliances can avoid added complexity and the need for customers to follow rates but feature higher up-front costs. + +#### 4. Programmatic decisions + +Although regulators do not need to craft rates for PTR programs (though they have to determine rebate levels), other CPP programmatic decisions, such as who the program applies to, the number of events, the length of customer advanced notice, and the size of discounts/rebates apply to both program types. + +**How many times per year/hours per event should programs feature?** CPP Programs vary in number and length of events. With extreme day pricing (EDP), for example, the peak rate is effective for 24 hours. Pacific Gas and Electric's CPP program events each last for six hours, the first three of which feature a moderate CPP rate of three times the normal peak TOU rate and the latter three of which feature a CPP rate that is five times the normal peak rate.[^24] Southern California Edison's CPP program includes 9 to 15 four-hour events.[^25] Gulf Power can call CPP events up to one percent of the time and they last for two hours.[^26] + +[^24]: [PG&E CPP Program](http://www.pge.com/mybusiness/energysavingsrebates/demandresponse/cpp/index.shtml) + +[^25]: [SCE CPP Fact Sheet](http://www.sce.com/NR/rdonlyres/672CFB73-002B-4CA5-8E86-B8F481E41A48/0/0909_CPPFactSheet.pdf) + +[^26]: [Gulf Power Energy Select](http://www.gulfpower.com/energyselect/the_rate.asp) + +Higher numbers of CPP or peak time rebate hours and events increase potential cost savings for the utility but also increase the burden on participants, requiring additional compensation or reducing the pool of willing participants. Regulators should assess the costs and benefits of various benefit/participation level scenarios observed in existing CPP and PTR programs. + +**How much should rates increase during CPP events?** CPP programs feature rates that are three to ten times higher than flat rates. CPP rates could approximate the highest spot market prices, which can exceed average electricity prices by ten fold. Alternatively, they could consider the long-run marginal cost or the cost of capacity. The California PUC found that "the critical peak price should represent the marginal cost of capacity used to meet peak energy needs plus the marginal cost of energy during the critical peak period."[^27] Pacific Gas and Electric Company increases prices by approximately five fold during CPP events. Gulf Power features CPP rates $0.285, roughly three times higher than flat rates. + +[^27]: CPUC Decision Adopting Dynamic Pricing Timetable and Rate Design Guidance for Pacific Gas and Electric Company, July 31, 2008, page 61. [CPUC Final Decision](http://docs.cpuc.ca.gov/PUBLISHED/FINAL_DECISION/85984.htm) + +**How large should discounts be for program participants?** Discounts vary by utility. For instance, Gulf Power's CPP rates, which couples with TOU rates, are 1.785 and 3.021 cents per kWh during low and medium hours, compared to 3.93 cents per kWh for base rates. Regulators should calibrate CPP rates to maximize the net of electricity savings minus the costs of discounts by reviewing the results of pilot programs. + +**How much advance notice should the CPP program provide?** Shorter lead times best enable utilities to respond to sudden changes in economic or reliability conditions. The less advance notice customers receive, however, the more onerous program participation becomes. Further, certain customers might not be able to reduce their consumption on short notice. Gulf Power provides a half hour of advance notice.[^28] In contrast, Portland General Electric and Southern California Edison contact customers the day before the CPP event.[^29] + +[^28]: [Gulf Power RSVP](http://www.gulfpower.com/pricing/pdf/rsvp.pdf) + +[^29]: [Portland General Electric Schedule 012](http://www.portlandgeneral.com/our_company/corporate_info/regulatory_documents/pdfs/schedules/Sched_012.pdf) + +**Should discounts come from the fixed, demand, or energy component of rates?** Discounts from the fixed component of rates provide predictable benefits for consumers and costs for the utilities. Further, they do not dampen incentives for participants to conserve electricity during non-CPP periods. The authors are unaware of any CPP programs featuring discounts to the fixed portion of rates or rebates. Southern California Edison's CPP program provides reduced monthly on-peak demand charges during the summer.[^30] The CPUC found that peak demand rates were redundant for customers with CPP rates, effectively double charging them for peak demand.[^31] In contrast, Gulf Power reduces the energy components of rates during most hours, advertising lower rates 87% of the time.[^32] + +[^30]: [SCE CPP for Large Business](http://www.sce.com/b-rs/large-business/cpp/) + +[^31]: [Gulf Power RSVP](http://www.gulfpower.com/pricing/pdf/rsvp.pdf) + +[^32]: [Gulf Power Energy Select](http://www.gulfpower.com/energyselect/index.asp) + +**Should CPP programs protect participants from higher bills?** Depending on the size of CPP discounts and rate increases, if CPP program participants do not reduce usage during CPP events, their bills could increase, absent protective mechanisms. Pacific Gas and Electric Company provides participants with an option ensuring that for the first 12 months, their monthly electric bills are limited to 100% of what they would have been under conventional rates. Such provisions could increase participation. They could, however, diminish participants' electricity consumption reduction by removing economic penalties for consumption during CPP events. Bill protection mechanisms could also apply to voluntary TOU or RTP rates. + +### F. Real-time pricing + +#### 1. Description + +Real-time pricing (RTP) rates adjust based on fluctuations in wholesale electricity costs. Such rates capture seasonality, time-of-day, weather, maintenance, and other factors that contribute to cost fluctuations. They fully match wholesale costs with retail rates, providing customers with accurate price signals. Unlike other EIRs, RTP rates are unpredictable. Consequently, most RTP programs target large customers who can best manage risks and consumption. + +[DIAGRAM DESCRIPTION: Gulf Power RTP Rate Schedule] + +Gulf Power offers an RTP rate schedule based on the system lambda, which is the marginal, variable production cost of electricity at a given level of system output.[^34] The schedule shows a table mapping system lambda ranges (in cents/kWh) to corresponding retail RTP rates, with rates increasing in steps as the system lambda rises. For context, Gulf Power's base (flat) rate is approximately 3.93 cents/kWh; its combined TOU/CPP program features rates of 1.785 cents/kWh during low-cost hours and 3.021 cents/kWh during medium-cost hours, with CPP events at approximately $0.285/kWh (roughly three times the flat rate). Gulf Power advertises lower rates than the flat rate 87% of the time. Specific system-lambda-to-retail-rate mappings are visible only in the PDF image. + +[→ See original PDF page 18 for rate values] + +[^34]: [Gulf Power RTP](http://www.gulfpower.com/pricing/pdf/rtp.pdf) + +#### 2. Benefits + +RTP rates fluctuate more than TOU or seasonal rates do, increasing customers' incentive to shift or reduce consumption during peak periods. Load shifting reduces average energy costs, particularly in organized markets, and forestalls infrastructure investments. Consumption reductions help states and utilities meet energy efficiency and carbon reduction goals or mandates. + +Real-time pricing programs enable utilities lacking fuel cost adjustment mechanisms to pass to customers the risk of wholesale electricity cost volatility. Such lower risk reduces their need to either (a) internalize cost risk, (b) acquire physical hedges like generators or long term power purchase agreements, or (c) purchase financial hedges. Utilities could pass some of these benefits to customers, as discussed in Section II.D. + +#### 3. Costs and concerns + +RTP requires AMI investments. Such investments can cost billions of dollars.[^35] RTP also increases rate complexity and unpredictability. Customers must observe and respond to frequent rate changes, disadvantaging unsophisticated customers lacking the willingness or ability to monitor rates. Additionally, unless they adopt hedging instruments, RTP customers cannot predict their electricity costs, inhibiting investment decisions. + +[^35]: By 2005-06, the average hardware cost per meter averaged $76. Capital costs related to communications infrastructure installation ranges from $125 to $150 per meter. [EPRI Advanced Metering](http://www.ferc.gov/EventCalendar/Files/20070423091846-EPRI%20-%20Advanced%20Metering.pdf) + +As discussed in section I.B.3, rate designs that affect consumer behavior increase utility revenue uncertainty. Conversely, reduced cost exposure risk could reduce revenue for certain utilities. + +#### 4. Programmatic decisions + +**What data should RTP rates use?** Real-time rates are easiest to administer in organized wholesale markets like those of PJM and MISO that offer locational marginal prices which reveal the day-ahead and real-time wholesale costs of electricity in each hour. For this reason, California declined to make real-time prices the default for certain customers until full rollout of the Market Redesign and Technology Upgrade.[^36][^37] Though perhaps less precise, real-time pricing programs can occur outside of organized markets. Gulf Power offers real-time pricing, calculating prices based on the system lambda. Some real-time rates are also based on a combination of time-of-use/seasonal rates and temperatures.[^38] Where available, market data is preferable to system lambda calculations or other methods because it provides the most accurate price information. + +[^36]: According to the California ISO, "MRTU is a comprehensive program that enhances grid reliability and fixes flaws in the ISO markets. It keeps California compatible with market designs that are working throughout North America and replaces aging technology with modern computer systems that keep pace with the dynamic needs of California's energy industry. The program launched March 31, 2009." + +[^37]: CPUC Decision Adopting Dynamic Pricing Timetable and Rate Design Guidance for Pacific Gas and Electric Company, July 31, 2008, page 13. [CPUC Final Decision](http://docs.cpuc.ca.gov/word_pdf/FINAL_DECISION/85984.pdf) + +[^38]: [SCE Real-Time Pricing](http://www.sce.com/b-rs/demand-response-programs/real-time-pricing-RTP-2.htm) + +RTP rates based on market data can draw from either hourly day-ahead prices or real-time prices. Day-ahead rates permit longer lead times for customers and reflect the cost of most power purchased in the market because utilities purchase most power in the day-ahead markets and use the real-time market for system balancing. Because utilities purchase most of their power in the day-ahead market, leaving the real-time market for balancing unexpected load and generation fluctuations, day-ahead RTP rates capture most of the cost variations borne by utilities. Real-time rates feature more variability and better reflect cost fluctuations though. Additionally, rates can be from the entire utility (or RTO footprint) or feature more granularity and be more location-specific. The latter features greater rate unpredictability and higher calculation complexity though. + +**Over what intervals should rates change?** AMI technology enables utilities to communicate rate changes at intervals as frequent as every five minutes. Intervals should align with the sophistication of customers. For instance, real-time rates for large industrial customers could change every 15 minutes, while rates for residential customers could change on a bi-hourly basis. More frequent intervals require more robust (and costly) AMI systems. + +Alabama Power and ConEd offer day-ahead real-time pricing rates, in which the utility informs customers the day before what rates will be for the next 24 hours. Rates reflect day-ahead, rather than real-time electricity prices. + +**What protections should RTP programs provide customers?** Several utilities offer optional price protection products to mitigate the risk of RTP fluctuations. These products include price caps, contract for differences (CfD), collars, and index swaps. Regulators should weigh the customer benefits of these mechanisms against the resulting reductions in the strength of RTP signals. Examples of these protection products include the following, the first three of which Alabama Power offers[^39]: + +- **Price caps** offer protection in the event that the average real-time price exceeds the cap price. In exchange for a premium, consumers can choose a price cap, time period, and the load amount to protect. Certain organized markets already feature price caps. + +- **Contract for differences (CfD)** is an arrangement in which consumers pay a predetermined fixed rate over a certain time period. CfDs offer customers added cost certainty. By serving as hedging instruments, however, CfDs reduce incentives to reduce peak consumption. + +- **Collars** create caps and floors for the average real-time rates over a specified period. They reduce customer exposure to extremely high rates. They also provide revenue floors for utilities. + +- **Index swaps** are financial arrangements linking an RTP swap price with a commodity price index. The swap price moves in conjunction with changes in the commodity price. + +- Certain retail electric providers offer products that supplement hourly pricing with a **physical hedge**. One such product is "block and index pricing." This pricing mechanism enables customers to purchase blocks of electricity at a fixed $/kWh rate. For any usage in excess of the block level, customers pay spot market prices. This mechanism combines elements of real-time pricing and inclining block rates.[^40] + +[^39]: [Alabama Power Price Protection Products](http://www.alabamapower.com/pricing/pdf/ppp.pdf) + +[^40]: Steven Braithwait, Dan Hansen, and Michael O'Sheasy, "Retail Electric Pricing and Rate Design in Evolving Markets," Edison Electric Institute, [EEI Report](http://www.eei.org/ourissues/electricitydistribution/Documents/Retail_Electricity_Pricing.pdf) + +--- + +## II. Common Implementation Considerations and Decisions + +For all EIRs, regulators must decide who they cover; whether they are mandatory; what, if any, steps to take to protect utilities from unpredictable or reduced revenues; and what kind of customer education utilities should conduct. + +### A. To whom should rates apply? + +Regulators should focus on providing EIRs to customers who best can respond to price signals. If applied to unresponsive customer groups, EIRs merely reallocate costs. Such reallocation could be appropriate where existing rates inaccurately allocate costs. Rates that do not lead to behavioral changes, however, would not provide aggregate benefits by reducing peak or total consumption. Cost reallocation, particularly absent net customer benefits, often proves divisive. + +As discussed in Section I.A, effective rate designs avoid undue complexity. Regulators should consider the effects of added complexity from EIRs on various customer groups. Seasonal rates are simple for even unsophisticated customers. In contrast, to benefit from RTP rates, customers must frequently monitor rates and change their consumption. RTP rates also make estimating bills more difficult for customers. Accordingly, some utilities, including Alabama Power, and Gulf Power offer real-time rates to only larger customers. Mandatory real-time rates should at least initially apply to only large customers who can predict their consumption, hedge, monitor electricity rates, and modify their consumption in response to rate changes. Nonetheless, certain utilities, including Ameren, have offered real-time pricing to residential customers.[^41] + +[^41]: [Ameren Residential RTP](http://www.ameren.com/Residential/ADC_RTP_Res.asp) + +CPP programs require customers to adjust their behavior on one day's notice or less. Large customers can best do so, particularly absent programmable thermostats that enable usage to automatically fall during CPP events. Accordingly, default rates for Southern California Edison customers with peak demand exceeding 200 kW include CPP.[^42] Despite this advantage, some utilities, including Gulf Power, have offered CPP to residential customers, though on an optional basis. TOU and inclining block rate complexity fall between seasonal rates and RTP rates. + +[^42]: [SCE CPP for Large Business](http://www.sce.com/b-rs/large-business/cpp/) + +Based on these considerations, regulators and utilities must determine whether or not to implement EIRs on a wide-scale or targeted basis. Regulators need to examine various customer groups' demand elasticities to estimate how they would respond to EIRs and the implications for their bills, discussed in Section IV. + +### B. Should rates be mandatory, opt-in, or opt-out? + +Mandatory rates maximize participation and include customers who must change their behavior to receive benefits or avoid added costs from new rates. Mandatory rates can prove politically difficult, however, as certain customer groups would likely see higher or less predictable electricity bills even if the aggregate costs fall. Certain rate designs, such as seasonal rates, are likely to cause less cost reallocation than other EIRs, such as RTP. + +Voluntary rates maximize customer choice and avoid forcing customers to use rates that increase their bills. Voluntary rates, however, feature lower participation rates, particularly by customers who consume large amounts of power during peak times. One study estimates that "voluntary rate structures generally attract a relatively small percentage of customers (less than 20 percent) unless marketed heavily by the utility."[^43] Voluntary rates can also lead to self-selection, where a disproportionate number of participating customers reduce their electricity bills without changing their behavior—at the expense of other ratepayers. + +[^43]: [EPA Clean Energy Guide](http://www.epa.gov/cleanenergy/documents/napee/napee_chap5.pdf) + +Optional rates include opt-out (default) or opt-in rates, where the existing rate design is the default. Opt-out rates feature higher participation levels than opt-in rates because many customers will not change their rates. In California, the Statewide Pricing Pilot featured an initial opt-in participation rate of 20%. At the conclusion of the program, only 10% of participants remained on the new rate. In contrast, Washington State implemented dynamic tariffs on an opt-out basis and reported that nearly 90 percent of customers participated.[^44] + +[^44]: Faruqui, Ahmad, and George, Stephen, "Demise of PSE's TOU program imparts lessons," _Electric Light and Power_, Jan. 2003. + +Some commissions or utilities have differentiated between customer classes when determining whether rates are mandatory, opt-in or opt-out. For instance, ConEd RTP rates are mandatory for customers with a peak demand of more than 15,000 kW but opt-in for other customers.[^45] Additionally, the California Public Utilities Commission mandated default (opt-out) CPP/TOU rates for large commercial, industrial, and agricultural customers but not residential customers. In the same decision, the CPUC determined that certain customers' rates would switch to default CPP/TOU rates after they possessed AMI for 12 months to allow them to observe their usage patterns in different seasons. It determined that smaller customers need this extra time, where larger customers do not. + +[^45]: [ConEd Voluntary Time Pricing](http://www.coned.com/energyefficiency/vol_time_pricing.asp) + +Where EIRs are optional, regulators can allow for EIRs to apply to only a portion of customers' demand to partially hedge against electricity cost variability. Accordingly, the California PUC found that "dynamic pricing rates should include a capacity reservation charge, or similar feature, that allows a customer to pay a fixed charge for a predetermined amount of its load and pay the dynamic price for consumption in excess of the reserved capacity."[^46] + +[^46]: CPUC Decision Adopting Dynamic Pricing Timetable and Rate Design Guidance for Pacific Gas and Electric Company, July 31, 2008, pages 21-26. [CPUC Final Decision](http://docs.cpuc.ca.gov/PUBLISHED/FINAL_DECISION/85984.htm) + +### C. How should regulators estimate changes in consumption and revenues after the introduction of EIRs? + +Regulators should estimate the short-term and long-term demand elasticities of various customer classes. They should examine existing literature on the demand elasticity of different customer groups—how their consumption changes in response to changes in costs. See Section V of Part Two for further discussion on customer elasticity. This information will inform how much they must iteratively adjust rates to ensure recovery of all fixed costs. + +### D. How should regulators address utility risk changes from EIRs? + +#### 1. Reduced utility cost volatility + +By aligning retail rates with wholesale electricity costs, EIRs can reduce utility exposure to wholesale electricity cost volatility. Real-time rates fully shift this risk to customers, followed by day-ahead real-time rates, CPP programs, time-of-use rates, and seasonal rates. Inclining block rates do not reduce utility risk. + +Many utilities have fuel or energy adjustment clauses that allow them to pass the cost of energy cost fluctuations to customers, although frequently with a lag. Some operate under decoupling mechanisms that separate sales from earnings. There is no reduction in the "hedging premium" where utilities can pass all energy costs through to customers or have decoupling mechanisms. According to the California PUC, "Because of the nature of long-term contracting and decoupling, there appears to be little cost-based justification to incorporate a hedging premium into rates at this time."[^47] + +[^47]: CPUC Decision Adopting Dynamic Pricing Timetable and Rate Design Guidance for Pacific Gas and Electric Company, July 31, 2008, page 52. [CPUC Final Decision](http://docs.cpuc.ca.gov/PUBLISHED/FINAL_DECISION/85984.htm) + +Utilities lacking such mechanisms, such as those under rate freezes, must bear all risk of electricity cost fluctuations if they offer flat rates. Utilities can internalize the risk, causing earnings fluctuations that can degrade their credit ratings. Alternatively, they can invest in physical hedges, including owning generation or entering into long-term power purchase agreements. Finally, utilities can use financial instruments to hedge, though such instruments increase total costs. Estimated risk premiums range from 11 to 15 percent of the value of consumer rates.[^48] + +[^48]: Ahmad Faruqui and Lisa Wood, "Quantifying the Benefits of Dynamic Pricing in the Mass Market," Edison Energy Institute, January 2008. + +If EIRs shift costs or risks to customers, regulators should consider requiring utilities to pass some of the benefit to customers. They can do so by lowering the return on equity or by allowing less hedging by the utility. + +#### 2. Increased utility revenue uncertainty + +To varying degrees, EIRs affect utility risk of fixed cost under-recovery because many utilities include fixed costs in variable rate components. Customers could reduce aggregate or peak consumption more than expected, causing sales to fall short of the revenue requirement. A utility could also over-recover if customer behavior does not change as predicted, if participation rates are low, or if CPP programs reduce energy costs more than sales revenues. + +Before attempting to mitigate risks, regulators should examine the degree of increased cost-recovery risk that utilities face due to EIRs. The more fixed costs that the energy component of rates includes, the more risk EIRs add. If EIRs increase utilities' risk, their credit ratings could fall, increasing the cost of capital. The following policies address the increased revenue risk associated with EIRs. This added risk is largely confined to the short term before customers have changed their behavior and utilities can estimate consumption and revenues as accurately as before the new rates. + +1. **Implement EIRs on a pilot basis.** Pilot programs, common to EIRs requiring AMI, allow utilities and regulators to gauge the level of customer response to rates and the effect on sales and costs. When evaluating data from pilot programs, regulators should examine whether participation was randomized. If "energy geeks" or those with low peak demand disproportionately participate in pilots, the data is not representative of the entire customer population. Pilots would not help estimate participation rates of optional rates. + +2. **Implement a decoupling adjustment**, which on an aggregate or per-customer level disconnects non-fuel cost recovery from consumption. Decoupling mechanisms eliminate revenue risk for utilities, although they can also eliminate potential revenue growth between rate cases due to growth in the number of customers and their demand.[^49] With decoupling, rates do not need to precisely estimate demand attrition resulting from EIRs. + +[^49]: For more information on decoupling, please see "A Rate Design to Encourage Energy Efficiency and Reduce Revenue Requirements" by David Boonin, [NRRI Publication](http://nrri.org/pubs/electricity/rate_des_energy_eff_SFV_REEF_july08-08.pdf) + +### E. How should utilities educate customers about EIRs? + +Whether EIRs are mandatory, opt-in, or opt-out, customer education is critical to their effectiveness. Customer sophistication tends to be lower for smaller customers. Many small customers do not examine their electricity bills or rate tariffs sufficiently to understand their rates. Thus, more outreach is needed for residential customers than for large industrials. Customers should (a) know about changes to their rates or new rate options and (b) understand how to minimize their bills under new rates. We recommend additional research on optimal ways to educate customers about new rates. + +--- + +## III. Efficiency-Inducing Rate Overlap + +Each of the five discussed EIRs can operate in isolation. With the exception of RTP rates, which can operate with only inclining block rates, each of these rate designs can also work in conjunction with the others. In some cases, combining EIRs magnifies their effectiveness and redistributional consequences. + +### A. Combining real-time pricing and critical peak pricing, time-of-use, or seasonal rates + +RTP is a substitute for CPP, TOU, and seasonal rates, capturing all of the electricity cost variations that they reflect. Thus, it cannot combine with these rates. + +### B. Combining critical peak pricing, time-of-use, and seasonal rates + +CPP, TOU, and seasonal rates each capture part of the variations in wholesale electric cost. Regulators can and have combined these three or combinations of two therein. Combined EIRs increase the degree of rate complexity, making them more problematic for less sophisticated customers. + +[DIAGRAM DESCRIPTION: Dominion Power Virginia Residential Combined TOU/Seasonal/CPP Rate Schedule] + +An example from Dominion Power providing Virginia residential customers with rates combining TOU, seasonal, and CPP elements.[^50] The schedule shows a multi-dimensional rate structure with different per-kWh energy charges for on-peak and off-peak periods within summer and non-summer seasons, plus an additional critical peak rate that applies during designated CPP events. The combined structure illustrates how three EIR types can layer together: seasonal variation (summer vs. non-summer), time-of-day variation (on-peak vs. off-peak), and critical peak pricing (CPP overlay). Specific per-kWh rate values for each combination of season, period, and CPP status are visible only in the PDF image. + +[→ See original PDF page 28 for rate values] + +[^50]: [Dominion Virginia Power Pricing Schedule](http://www.dom.com/dominion-virginia-power/customer-service/energy-conservation/pdf/pricing_schedule.pdf) + +### C. Combining inclining block rates and all other EIRs + +Inclining block rates do not attempt to align rates with wholesale electricity costs. Rather, they encourage customers to limit their overall consumption. As such, they are not redundant with any of the other EIRs. Combining inclining block rates with other rates increases rate complexity though. Inclining block rates can combine with other EIRs through separate energy charges, which vary with the block. Hypothetically, instead of TOU rates featuring rates of 8, 12, and 16 cents per kWh depending on the time period, they could be 4, 8, and 12 cents with a second energy charge that is 0, 3, or 9 cents per kWh depending on the block. + +[DIAGRAM DESCRIPTION: Hawaiian Electric Company Combined TOU and Inclining Block Rate Schedule] + +Proposed rates from the Hawaiian Electric Company that combine TOU and inclining block rate elements.[^51] The schedule shows per-kWh energy charges that vary along two dimensions simultaneously: (1) time-of-day period (e.g., on-peak, off-peak) and (2) monthly consumption block (lower vs. higher usage tiers), creating a matrix of rates. The combination means a customer's effective rate depends on both _when_ they consume electricity and _how much_ they consume in total. This is one approach to combining IBR with TOU — alternatively, the combination can use separate energy charges that vary by block (e.g., a base TOU charge of 4, 8, and 12 cents/kWh depending on the time period, plus a second energy charge of 0, 3, or 9 cents/kWh depending on the consumption block). Specific per-kWh rate values in the HECO matrix are visible only in the PDF image. + +[→ See original PDF page 29 for rate values] + +[^51]: See Exhibit HECO-106 of HECO's rate filing in Docket No. 2008-0083 (2008). + +Combining inclining block rates with other EIRs magnifies customer bill reductions or increases, depending on their load profiles. As noted in Section I, inclining block rates could increase bills for large, poor households who feature high electricity usage but cannot improve their energy efficiency. Hypothetically, such a household could be low-income (and thus unable to improve efficiency); occupy a large, older house; and include persons who are home during the daytime, necessitating peak-period HVAC usage. If inclining block rates combine with TOU rates, this customer would see bill increases associated with inclining block rates magnified because it cannot respond to peak TOU rates. + +### D. Combining critical peak pricing and demand response programs + +Certain utilities or RTOs offer demand response programs through which utilities can require certain customers to curtail their load in emergency situations in exchange for rate discounts or direct compensation for each event. Although some customers might participate in CPP programs and not conventional demand response programs because they want to maintain control of their consumption, the two types of programs target similar customers. Some utilities or RTOs offer economic demand response programs where participants receive a credit for reducing load but their rates do not change during critical periods. These programs' critical events are likely to be the same as CPP events. Participation in CPP programs is thus likely to be low in the presence of other demand response programs. + +--- + +## IV. Secondary Effects of Efficiency-Inducing Rates + +### A. Redistributive consequences + +Rate design changes often prove contentious because they reallocate costs, with each customer class seeking to bear as little cost as possible. Table 2 below describes which customers would likely benefit or suffer under various EIRs. + +### Table 2. EIR Redistributive Consequences + +| Rate design | Customers likely to see reduced bills | Customers likely to see additional bills | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Inclining block rates | Customers who consume low amounts of electricity, such as small households and customers with gas heating and hot water; Customers able to invest in energy efficiency (EE) measures or gas appliances | Customers who consume large amounts of electricity such as large families; Customers who are unable to reduce consumption or invest in EE measures or gas appliances (e.g., poor customers using space heaters) | +| Seasonal rates | Customers whose consumption is not concentrated in the peak season; Customers who can shift some of their consumption to the off-peak season; Customers able to invest in EE measures specific to the peak season | Customers whose consumption is heavily in the peak season (e.g., ski resorts) who cannot shift consumption to the off-peak season; Customers unable to invest in EE measures specific to the peak season (e.g., poor customers) | +| Time-of-use rates | Customers who have low daytime loads (e.g., daytime workers); Customers capable of shifting load from peak to off-peak times (e.g., manufacturers or mining customers); Customers capable of implementing EE measures specific to peak hours (e.g., efficient air conditioners) | Customers with high daytime usage (e.g., office buildings); Customers with low ability to shift load from peak to off-peak times (e.g., retail stores) or reduce peak consumption | +| Critical peak pricing | Sophisticated customers capable of observing rate changes and adjusting their consumption | _(not specified in source)_ | +| Real-time pricing | Sophisticated customers able to monitor electricity rates and respond to changes | Unsophisticated customers unable to monitor rates and respond to changes; Customers with fixed electricity usage or high usage during peak periods | + +### B. Renewable energy implications of EIRs + +Numerous states have or are considering implementing policies to encourage distributed renewable energy systems. Most distributed generation systems are solar PV, small wind, or anaerobic digestion generators. They offset their owners' electricity consumption. Additionally, in most states, customers with distributed generators can sell excess electricity back to the grid through net metering programs. Some states also provide tax credits, utility rebates, low-interest loans, and other incentives for distributed renewable energy generators. Many states have created markets for tradable renewable energy credits in conjunction with renewable portfolio standards. Some offer tax credits (sales, property, or income) or rebates to encourage development. Policymakers should evaluate various EIRs' effect on the value of renewable energy generation to calibrate such incentives, such that they are sufficient but not excessive. + +Most parts of the U.S. feature peak electricity consumption and wholesale electricity costs during summer, weekday afternoon hours. TOU rates, seasonal rates, and real-time pricing (and, to a lesser extent, critical peak pricing) all add value to behind-the-meter renewable energy generators that produce a disproportionate amount of their electricity during these hours. Solar generators in particular match this load profile. + +[DIAGRAM DESCRIPTION: Solar Generation Profile vs. Summer Daily Load Profile] + +A graph showing a solar generation profile overlaid on a typical summer daily load curve for a single summer day.[^52] The solar generation curve peaks during midday hours (roughly 10 a.m. to 4 p.m.), closely matching the period of highest electricity demand and wholesale costs. This alignment demonstrates why TOU and seasonal rates increase the economic value of solar generation. + +[→ See original PDF page 33 for visual rendering] + +[^52]: [California Solar Dilemma Blog Post](http://i-r-squared.blogspot.com/2007/07/california-solar-dilemma.html) + +Biomass generators such as anaerobic digesters are dispatchable, such that they can provide their electricity when it is most valuable. + +In contrast, wind turbines in most locations produce the most electricity during winter or shoulder months and in the morning or at night, when electricity is least valuable under EIRs. States promoting small wind development (currently a small percentage of overall wind generation) should consider the effects of seasonal, TOU, and RTP rates by examining typical production profiles of wind turbines and calculating their value under different rate designs. + +At the margins, CPP programs could make dispatchable, behind-the-meter generation like anaerobic digestion generators more valuable by enabling customers to switch from taking electricity from the grid to using their own generation during CPP events. CPP events are infrequent enough, and the corresponding discounts small enough, however, that CPP rates do not markedly affect project economics. + +Inclining block rates can increase or decrease the value of distributed generation, depending on the customer's electricity consumption levels and the block cutoffs. If inclining block rates reduce rates at unchanged consumption levels for the customer demographics most likely to purchase solar PV or small wind generators, these customers will be less likely to purchase them. Conversely, if these customers' rates, absent changes in electricity consumption, increase under inclining block rates, distributed generation becomes more attractive. States intending to promote distributed generation should examine what customer demographics purchase most distributed renewable energy generators and how their incentives would change under potential inclining block rates. + +--- + +# Part Two: Time-of-Use Rate Implementation + +## Table of Contents (Part Two) + +- Introduction +- I. Step 1 – Obtain Data +- II. Step 2 – Select Seasons +- III. Step 3 – Set Hours + - A. Cluster analysis + - B. Other statistical analyses +- IV. Step 4 – Set Rates +- V. Step 5 – Estimate the Effects of TOU Introduction +- VI. Step 6 – Conduct Sensitivity Analysis +- VII. Step 7 – Finalize TOU Pricing +- VIII. Conclusions + +--- + +## Introduction + +Policymakers face a number of important decisions when designing dynamic rates or evaluating proposed dynamic pricing schedules. These decisions are listed in Part One, sections I and II. The purpose of this document is to provide policymakers with practical guidelines for implementing those decisions. In this document we focus on the process of designing time-of-use (TOU) rates. Two other types of dynamic pricing—seasonal rates and critical peak pricing—are designed in a process that is similar (with necessary variations) to the process discussed below. The designs of inclining block rates and real-time-pricing differ more significantly from this process. + +### Time-of-Use Pricing Design: Seven Steps + +| Step | Description | +| ------ | ------------------------------------ | +| Step 1 | Obtain data | +| Step 2 | Select seasons[^s1] | +| Step 3 | Select time-of-day periods[^s2] | +| Step 4 | Set rates | +| Step 5 | Estimate effects of TOU introduction | +| Step 6 | Conduct sensitivity analysis | +| Step 7 | Finalize TOU pricing | + +[^s1]: The steps represent the typical design process of TOU pricing; in some cases the utilities choose not to separate out different seasons, thus eliminating Step 2. + +[^s2]: Seasonal pricing can be designed using the same process minus Step 3. The design of critical peak pricing requires two additional steps: the selection of critical peak hours and their pricing. + +--- + +## I. Step 1 – Obtain Data + +TOU rates and other dynamic pricing methods attempt to reflect the underlying costs of electricity more precisely than do traditional rate structures (i.e., rate structures based on embedded costs rather than running costs) using the same average rates for every hour of the year rather than rates that vary as costs vary. A utility's costs change every hour (in fact, even more frequently). A rate structure with 24 daily changes, however, would confuse customers. Grouping hours allows the analyst to avoid such excessive precision while still producing rates that reflect cost changes. + +Careful examination of a utility's data should allow the grouping of hours into different categories to create a TOU schedule that reflects variations in demand and costs. To design reasonable grouping, the analyst can employ two types of data: (1) data on the utility's loads and (2) data on the utility's marginal costs. In principle, analyses of both types of data should provide similar results: Higher loads are typically associated with higher marginal costs, since utilities have to use generation facilities with higher operating costs to meet larger demand during peak hours. In practice, however, the relationship between load level and cost level is not always proportional.[^p1] It is advisable, therefore, to use both types of data in the analysis if this information is available. + +[^p1]: For example, higher marginal costs in a particular period might result from an increase in fuel costs rather than an increase in demand. + +The cost data is either (a) the cost of a utility's production of electricity (if the utility owns the generation necessary to serve its load), or (b) the cost of purchasing electricity in organized wholesale supply markets or under bilateral contracts (if the utility purchases the necessary electricity). In organized wholesale markets the data can be obtained from a relevant ISO or RTO. If a market is effectively competitive, wholesale prices should reflect the actual marginal cost—the cost of producing another kilowatt-hour[^p2]—including how that cost varies across different seasons and hours of the day. The applicable data is the average locational marginal price paid by the utility, on average, for each kWh for each hour of the year. The zonal LMP includes both the cost of the electricity and the congestion. In states with non-organized markets (i.e., bilateral contract markets) the analyst can obtain the data from the utility.[^p3] Even where the utility procures some of its energy through fixed power purchase agreements, rates should reflect the costs paid through organized markets. LMP payments are the marginal cost, and thus the cost to the utility of additional consumption or savings from reduced consumption. + +[^p2]: A wholesale electricity market exists when competing generators offer their electricity output to retailers. The mere fact that sellers compete does not mean that the market is effectively competitive, however. Remember that it is possible to exercise market power that can raise prices above real economic costs. In fact, when prices in the market vary from marginal costs, that fact is a signal that the market is not effectively competitive. For detailed background on market power and its relation to marginal cost, see Rosenberg (2007). + +[^p3]: Appendix A contains an example of a data request that can be sent to a utility. + +It is advisable that the data cover each hour of the year. Having data at the hourly level allows for maximum precision in grouping. Having data for a couple of recent years, moreover, will mitigate the effects of anomalies associated with any particular year.[^p4] + +[^p4]: Examples of such anomalies include unusual weather conditions, blackouts, and exercise of market power at the wholesale level. + +--- + +## II. Step 2 – Select Seasons + +Consumer behavior varies with seasons—in most places people consume more electricity during midday hours in the summer months than during those same hours in the winter months. The composition of peak and off-peak hours of the day depends on the season. Thus, before identifying time-of-day pricing periods we need to separate months with similar patterns of consumption and marginal costs into distinct seasons. Then we can separate time-of-day periods within each season. The alternatives to having different seasons are (a) to create a different TOU schedule for each month, or (b) to apply the same TOU schedule for the whole year, ignoring any seasonal differences. Comparing these alternatives, seasonal grouping offers a better balance between precision and convenience. The example below illustrates how seasons are selected for a hypothetical Utility X.[^p5] + +[^p5]: The analysis uses marginal cost data. Load data can be employed in a similar fashion. + +[DIAGRAM DESCRIPTION: Figure 1. Marginal Costs of Utility X] + +A line graph showing the distribution of hourly marginal costs (cents/kWh, vertical axis, ranging from 0 to 60) for each month of the year for Utility X, plotted against the 24 hours of the day (horizontal axis).[^p6] The graph indicates the presence of two different patterns: winter months (November through March, shown as thick lines of light colors) display a distribution with two peak periods (morning and evening hours), while summer months (April through October, shown as thin lines of darker colors) display a distribution with one peak period (midday hours). This visual distinction supports the identification of two distinct seasons: winter and summer.[^p7] + +The shapes of the distributions are related to variations in temperature and the usage level of cooling/heating appliances. The summer period features one peak period (midday hours), which has the highest marginal costs due to midday air conditioning load driving dispatch of expensive peaking units. The winter months feature two peaks (morning hours and evening hours) due to high demand for heating during the morning hours (when people wake up) and during the evening hours (when they come home from work). + +[→ See original PDF page 39 for visual rendering] + +[^p6]: Each data point is an average of the cost of a particular hour for each of the 30 days of the month. The data appear in Appendix B. + +[^p7]: This stylized example includes a substantial difference between the summer and winter months. It is possible to have more than two seasons for other utilities. For instance, some regions feature distinct fall and spring shoulder months with lower peak loads. + +The analyst can examine a number of statistics in order to identify which month belongs to which season when it is not clear from the graphical representation. The analyst can calculate the coefficients of correlation and overall distances between distribution curves pairwise for adjacent months. + +Let us say we have to decide where the month of March belongs: to the winter period that includes November through February or to the summer season of April through October. We can look at the coefficients of correlation[^p8] between March and February (the closest month of winter) and between March and April (the closest month of summer). The coefficient of correlation between March and February is 0.99[^p9], which is much higher and more statistically significant than the coefficient of correlation between March and April (0.33 and statistically insignificant). The coefficients indicate that the patterns of consumption in March and February are more similar to each other than the patterns of consumption in March and April. + +[^p8]: The coefficient of correlation measures how well a trend in one variable follows the same trend in another variable. The correlation coefficient is a number between 0 and 1. If there is no relationship between the two variables, the correlation coefficient is close to 0; the stronger the relationship, the higher the correlation coefficient will be. Coefficients of correlation can be readily calculated using Microsoft Excel or any major statistical software package. + +[^p9]: The coefficients can be obtained with the help of any statistical package or MS Office Excel. + +We can also calculate the sums of the squared distances between corresponding points of the distributions (the points that represent marginal costs at the same hour of the day in different months).[^p10] This statistic is 278.38 for February and March, and 6,110.97 for March and April. The smaller distance between the February distribution and March distribution indicates that February is closer to March than April is. Thus, we place March in the winter season. + +[^p10]: The statistic shows how close the distributions are to each other. The statistic is calculated as the square of the sum of the differences in the values of marginal costs in the same hours of the day in two different months. The statistic can be calculated using Microsoft Excel or any major statistical software package. + +--- + +## III. Step 3 – Set Hours + +After identifying seasons, the next task is to group hours of the day into distinct time-of-day periods within each season. The analyst has to select the optimal number of time-of-day periods, as well. Such periods should reflect the variations in marginal costs across the 24-hour period. The TOU schedule should not only reflect the cost of electricity supply; it also should induce a cost-reducing response from customers. The schedule therefore should (a) be accurate (in terms of the relationship between price and cost), (b) be convenient for customers, and (c) provide enough incentives for customers to adjust their behavior. + +The analyst can employ a number of statistical techniques to group hours. An example of the application of one of these methods, cluster analysis, is provided below. We briefly introduce other methods later in the text. + +### A. Cluster analysis + +A cluster analysis is a technique used in various disciplines to assign a set of observations (data points) to subsets (clusters), such that observations in the same cluster are similar in some characteristics (in our case, marginal costs or load levels), but have different characteristics between the clusters.[^p11] The example below illustrates how a cluster analysis is applied to identify periods of the day within the winter season for Utility X. In our analysis, an observation is the marginal cost at a particular hour of the day. A cluster analysis can be based on one or more variables (characteristics) per observation. For example, hourly marginal costs on an average day could constitute the basis for a one-variable cluster analysis. Alternatively, the analyst can use additional variables to reflect the variation between clusters in electricity demand and costs more accurately. In our example, cluster analysis is based on the following three variables: marginal costs on a minimum peak day[^p12] of each hour of the day of Utility X, costs on a maximum peak day, and costs on an average day (the average of 30 days of the month). With this three-variable cluster analysis[^p13], we are seeking to group hours of the day, within a season, such that within each group of hours there is a similarity of marginal costs; and such that between each group, there is a difference in these costs. + +[^p11]: Cluster analysis can be carried out with the use of a statistical package such as STATA, SAS, or SPSS. The application of this technique is relatively simple; examples of codes can be found in the STATA manual or STATA help function. For a discussion of the mathematical basis for clustering analysis, see Brian S. Everitt et al., _Cluster Analysis_ (2001); Hartigan, J., _Clustering Algorithms (Probability & Mathematical Statistics)_ (1975); or Charles Romesburg, _Cluster Analysis for Researchers_ (2004). + +[^p12]: The marginal costs during the peak period on the minimum (maximum) peak day are at their lowest (highest) compared with other days of the month. + +[^p13]: Data for the hypothetical Utility X that is used in the cluster analysis are provided in Appendix B. + +[DIAGRAM DESCRIPTION: Figure 2. Cluster Analysis Output — Dendrogram for Winter Cluster Analysis] + +A dendrogram showing the results of the three-variable cluster analysis for Utility X's winter season.[^p14] The vertical axis shows the L2 dissimilarity measure (ranging from 0 to 50), and the horizontal axis lists the 24 hours of the day. The dendrogram shows hours grouped hierarchically by similarity of marginal costs. At the lowest level, individual hours form their own clusters; at higher levels, similar hours merge together. The dendrogram indicates two reasonable grouping solutions:[^p15] + +- **Two-cluster solution**: lower-cost hours (1, 2, 3, 4, 12, 13, 14, 15, 23, 24) and higher-cost hours (5, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 22). +- **Four-cluster solution**: off-peak hours (23, 24, 1, 2, 3, 4), peak hours (morning: 5, 6, 7, 8, 9, 10 and evening: 16, 17, 18, 19, 20, 21), shoulder hours (12, 13, 14, 15), and two sporadic hours (11 and 22) that do not belong to the other categories. + +The height of the vertical lines indicates the strength of the clustering: longer vertical lines indicate more distinct separation between groups. + +[→ See original PDF page 43 for visual rendering] + +[^p14]: A dendrogram graphically presents how observations are grouped together at various levels of dissimilarity. At the very bottom of the dendrogram, each observation constitutes its own cluster. The observations continue to unite until, at the top of the dendrogram, all observations are grouped together. The height of the vertical lines indicates the strength of the clustering: The longer the vertical lines, the more distinct the separation between the groups. The Y axis measures the degree of dissimilarity. The X axis lists hours that are included in different clusters. A dendrogram can be obtained after performing a cluster analysis in STATA or another statistical package. + +[^p15]: Cluster-analysis stopping rules can help to determine the optimal number of clusters when it is difficult to do this based on graphical results alone. A stopping-rule value is computed for each cluster solution (e.g., at each level of the cluster tree). Larger values (or smaller, depending on the rule) signal more distinct clustering. These values help to decide when to stop clustering. In general, clustering should be stopped when the clusters are not very different from each other. + +Other groupings of hours are also possible, with more precise grouping of hours found closer to the bottom of the dendrogram. It is not feasible, however, to offer customers a pricing schedule that asks them to pay a different rate every hour or every couple of hours.[^p16] + +[^p16]: Other groupings of hours are also possible, with more precise grouping of hours found closer to the bottom of the dendrogram. It is not feasible, however, to offer customers a pricing schedule that asks them to pay a different rate every hour or every couple of hours. + +The four-cluster solution allows us to price electricity at a more precise level; thus, the following discussion focuses on this solution. The four-cluster solution suggested by the analysis consists of the following groups: off-peak hours (23, 24, 1, 2, 3, 4), peak hours (morning: 5, 6, 7, 8, 9, 10 and evening: 16, 17, 18, 19, 20, 21), shoulder hours (hours 12, 13, 14, 15), and two sporadic hours that are not adjacent to each other and do not belong to the other categories (11 and 22). + +A careful look at the composition of hours in each category reveals an important issue in designing TOU pricing schedules: the **tradeoff between precision of grouping and adjacency of hours**. The grouping suggested above separates two nonadjacent hours, 11 and 22, from the other four clusters. Pricing these two hours separately would create an inconvenience (and higher complexity) for customers. Instead of blindly following the results of a technical analysis, the analyst should apply her subjective opinion—and her common sense—to place these hours in other categories. In our case, hours 11 and 22 can be added to the peak hours, because, according to Figure 2, at the higher level of aggregation these hours are clustered with the peak hours. + +As a result of the preceding discussion, we can suggest the following TOU schedule for Utility X. + +### Table 1. TOU Pricing Schedule + +| Period | Off-Peak | Shoulder | Peak | +| ------------------------------------- | ---------------- | ---------------- | ---------------------------------------------------- | +| Hours | 11 p.m. - 4 a.m. | 12 p.m. - 3 p.m. | Morning: 5 a.m. - 11 a.m.; Evening: 4 p.m. - 10 p.m. | +| Range of marginal costs (¢/kWh)[^p17] | 10.1 - 14.2 | 20.2 - 33.4 | 39.5 - 45.7 | +| Average (¢/kWh) | 10.8 | 21.0 | 42.4 | + +[^p17]: The data can be found in Appendix B. + +One question that arises from the examination of the above schedule is whether there should be a distinct shoulder period. Again, the more periods we have, the less convenient and more complicated the TOU rates become for a customer to follow. At the same time, if consumers do not see enough difference in the prices they pay in different periods, they are not going to shift their consumption between periods. To address this question, one could examine the ratio of marginal costs in different periods. This ratio is useful because it reveals the distance between rates in different periods. In the case of Utility X, this ratio is approximately **1:2:4** (off-peak : shoulder : peak). + +While there is no rule of thumb that tells us what exactly the ratio between the periods should be in order to determine the appropriateness of creating a separate pricing period, the results of previous studies provide some guidance. A Wisconsin TOU pricing experiment showed that residential consumers reacted by switching consumption from peak to off-peak during the summer months by about 11 to 13% in response to a peak-to-off-peak price ratio of 2:1. When the ratio was 10:1, the response was 15 to 20%.[^p18] Faruqui and George (2005) found that TOU rates with a peak-to-off-peak ratio of around 2 to 1 produced peak-period reductions in the 5% range during California's Statewide Pricing Pilot. In general, it seems that **if the ratio between any two periods is less than 1 to 2, it does not make sense to separate these periods** due to potentially insignificant customer responses. + +[^p18]: Caves, D.W., and L.R. Christensen (1983). + +The other question is how to price weekend and holiday hours. Most of the utilities that employ TOU pricing categorize these hours as off-peak and price them accordingly. In some cases, however, the marginal costs of electricity on weekend and holiday afternoons are much closer to weekday shoulder or peak prices. In addition, weekend and holiday loads represent a non-trivial fraction of a utility's overall load; thus it is advisable to price these hours more precisely in order to optimally align rates with costs and correspondingly modify consumer behavior. With weekend and holiday hours separated from the other off-peak hours and shoulder hours (so as to give them their own price), a pricing schedule might take the following form: + +### Table 2. TOU Pricing Schedule (weekend/holiday hours are priced separately) + +| Period | Off-Peak | Shoulder | Peak | +| ------ | ----------------------------------------------------------------- | ------------------------------------------------------------------ | -------------------------------------------------------------- | +| Hours | Weekdays: 11 p.m. - 4 a.m.; Weekends & holidays: 12 a.m. - 9 a.m. | Weekdays: 12 p.m. - 3 p.m.; Weekends & holidays: 10 a.m. - 11 p.m. | Weekdays: Morning: 5 a.m. - 11 a.m.; Evening: 4 p.m. - 10 p.m. | + +### B. Other statistical analyses + +Cluster analysis is not the only technique that allows identification of TOU periods. We briefly describe other available statistical methods next.[^p19] In general, all these methods serve to identify groups of hours that have similar characteristics (loads or costs) within themselves, but different characteristics between them. + +[^p19]: A more detailed discussion of these methods, along with their applications, can be found in Conway (1982). + +- **The ANOVA method** helps to select breakpoints between different periods that make (a) the variation of costs within TOU periods as small as possible, and (b) the variation of costs between TOU periods as large as possible. + +- **The equal variance method** identifies breakpoints that make variances of costs in different periods the same to ensure that costs are equally homogeneous in each period. + +- **The minimax method** helps to determine breakpoints that make the difference between the highest and lowest costs in each TOU period as small as possible. + +All of these techniques are supposed to provide guidance in designing time-of-use rates. At the end of the day, however, many decisions about rate design are left to the analyst's discretion. Blind application of the results of a technical analysis could lead to ineffective pricing schedules. Examples of ineffectiveness would include TOU schedules that fail to provide incentives to consumers (i.e., schedules that set rates at a very similar level for different periods) and schedules with an unreasonably large number of periods. + +--- + +## IV. Step 4 – Set Rates + +Having determined the appropriate number and timing of the periods, the next step is to decide the rates for each period. Rates can be based on average marginal costs (i.e., the average of the hourly marginal costs within the period) or average weighted (by load) marginal costs in each period. There is a problem, however, with basing the rate on marginal cost: The utility will not recover its revenue requirements. Marginal cost pricing recovers only marginal cost—the cost of running the utility for that hour, mostly energy and some other variable costs. The utility incurs other costs, which it normally recovers through customer charges and demand charges (i.e., charges that do not vary with hourly usage). There are also costs associated with the introduction of TOU pricing, such as the installment and maintenance of advanced meters and customer education. The difference can be covered by either scaling up[^p20] rates based on marginal costs or through fixed charges (as with traditional rates). In choosing the method of recovering these other costs, the analyst should take care not to disturb or blur the incentives provided to customers by dynamic pricing (i.e., the incentive to shift load to the lower-cost periods). These incentives can be preserved by keeping the ratio between different periods' rates close to the ratio between marginal costs in these periods. + +[^p20]: "Scaling up" means proportionally increasing rates in all the periods while keeping the same ratio between the periods. + +The purpose of TOU pricing is to induce changes in consumers' behavior. The TOU introduction will affect a utility's loads and revenues. Thus, it is important to conduct forecasts before making a decision about the launching of a new pricing scheme. The next section discusses how to estimate the effects of TOU introduction on utility's loads and revenues. + +--- + +## V. Step 5 – Estimate the Effects of TOU Introduction + +Having developed the TOU schedule and rates, the next step is to estimate the effects of a TOU schedule's introduction on a utility's loads and revenues. This step will compare TOU rates to traditional rates to reveal whether expected revenues from TOU pricing meet revenue requirements. This information will also allow the comparison of TOU pricing to alternative dynamic rates, such as seasonal pricing, critical peak pricing, real-time pricing, and inclining block rates. + +This task requires estimates of customers' price elasticity of electricity demand. The price elasticity of demand is a measure of the sensitivity of the quantity demanded to changes in price. While numerous studies have examined the elasticity of demand for electricity, the results of these studies are inconclusive, with wide variations in results. + +An accurate estimation of the TOU effects requires information on two types of elasticity: "own-price" elasticity[^p21] and "cross-price" elasticity.[^p22] In addition, customer classes likely differ in their response to a rate change. For example, commercial customers who operate businesses during the day might be less willing to reduce the timing or amount of electricity consumption than residential customers. Thus, estimates of demand elasticities for different groups of customers are required. A summary of elasticities based on a review of the existing studies is provided in Appendix C. + +[^p21]: Own-price elasticity of demand is a percentage change in quantity demanded in response to a percentage change in price of the same good or service. For example, a morning own-price elasticity of -0.25 indicates that if the price of electricity in this period goes up by 1%, the demand in this period decreases by 0.25%. A review of existing studies reveals that most estimates of elasticities are found in the range of -0.12 to -0.35. EPRI White Paper (2008) provides a review of the most recent studies of customer response to dynamic rates programs. + +[^p22]: Cross-price elasticity of demand is a percentage change in the demand for good A that occurs in response to a percentage change in the price of good B. Let us view electricity in the morning as a good that is distinct from electricity in midday. The question is whether the customer would substitute one for another, just as a traveler might substitute a train ride for an airplane ride. If the cross-price elasticity between morning and midday is -0.10, then if the price of electricity in the midday period goes up by 1% the demand in the morning hours decreases by 0.10%. This customer does not view morning consumption as a substitute for midday consumption; rather, she views these two goods as complements and consumes them together. Thus, if the price during one period increases, the customer will reduce consumption during both periods. + +The example below illustrates how to apply elasticities to calculate the effect of TOU on the loads and revenues of Utility X's residential customers. Calculations for other types of customers can be carried out in a similar fashion using corresponding elasticity estimates. + +Faruqui and George (2002) provide the following matrix of own- and cross-price elasticities that the analyst can employ in order to estimate the response of residential customers. + +### Table 3. Elasticity Matrix for Residential Customers + +| | Morning | Midday | Evening | Night | +| ----------- | --------- | --------- | --------- | --------- | +| **Morning** | **-0.25** | -0.10 | -0.05 | 0.15 | +| **Midday** | -0.10 | **-0.25** | -0.05 | 0.05 | +| **Evening** | -0.10 | -0.05 | **-0.20** | 0.05 | +| **Night** | 0.15 | 0.05 | 0.15 | **-0.15** | + +_Source: Faruqui and George (2002), p. 55. In order to find out how demand for electricity in the morning period (1st good) is going to respond to changes in the price of electricity in the evening period (2nd good) we first locate the 1st good (morning period) in the horizontal cells, and then find a corresponding 2nd good (evening period) in the vertical cells. Following this procedure gives us the value of -0.05._ + +Own-price elasticities can be found in the shaded (bolded) cells on the diagonal; cross-price elasticities in the off-diagonal cells. In some periods, the values of cross-price elasticities are negative; in other periods, positive. A negative sign indicates that two goods (in our case, consumption of electricity) are complements; a positive sign means that two goods are substitutes. + +According to the matrix, electricity demand in the morning, midday, and evening periods are complements for each other and substitutes for the night period. For instance, the cross-price elasticity between morning and night of 0.15 shows that when the price of electricity in the night period goes up by 1%, people start substituting night-period electricity for morning-period electricity by 0.15% (for example, by changing the times at which they do laundry or cook). + +Next, the analysis applies elasticity estimates to Utility X's residential loads and marginal costs to estimate the effect on the utility's loads and revenues. The following example illustrates the calculation process. + +### Table 4. Estimation of TOU Effects on Utility X's Loads and Revenues + +| TOU period | Load (MWh) | Marg cost (¢/kWh) | Trad rate (¢/kWh) | % change | Trad revenue ($) | Effect on load (night) % | Effect on load (morning) % | Effect on load (evening) % | Effect on load (shoulder) % | TOU load (MWh) | TOU revenue ($) | +| ----------------- | ----------- | ----------------- | ----------------- | -------- | ---------------- | ------------------------ | -------------------------- | -------------------------- | --------------------------- | -------------- | --------------- | +| Night (off-peak) | 32,384 | 10.8 | 35.0 | -69.1 | 11,334,400 | 10.4 | -10.4 | -3.5 | -3.5 | 37,149 | 4,012,100 | +| Morning (peak) | 144,716 | 42.4 | 35.0 | 21.1 | 50,650,600 | 3.2 | -5.3 | -2.1 | -2.1 | 126,316 | 53,558,151 | +| Evening (peak) | 151,959 | 42.4 | 35.0 | 21.1 | 53,185,650 | 3.2 | -1.1 | -4.2 | -1.1 | 140,106 | 59,405,028 | +| Midday (shoulder) | 41,903 | 21.0 | 35.0 | -40.0 | 14,666,050 | -2.0 | 4.0 | 2.0 | 10.0 | 43,316 | 9,096,303 | +| **Total** | **370,962** | | | | **129,836,700** | **14.7** | **-12.7** | **-7.8** | **3.4** | **346,887** | **126,071,582** | + +In this example, the traditional flat rate charged by Utility X is 35 cents/kWh. New TOU rates are set at the average marginal costs (from Table 1). The values of elasticities from Table 3 are applied. + +The following explains how to estimate the effect of the TOU pricing on the load in the shoulder period: + +- **First, we calculate the conservation effect.** The price in the shoulder period decreases from 35 cents to 21 cents, or by 40%. This percentage change is then multiplied by the shoulder own-price elasticity of -0.25, which gives an increase in the shoulder load of about 10%. + +- **In addition, we have to take into account another effect—the effect of load shifting between periods of the day.** + - The night price decreases from 35 cents to 10.8 cents, about 69%. Night and midday shoulder periods are substitutes with a cross elasticity of 0.05; thus the shoulder load decreases by 3.5% in response to the decrease in the night price. + - The evening price increases by 21.1%, and the evening and shoulder are complements with a cross elasticity of -0.05; thus the effect on shoulder load is a decrease of 1.1%. + - The morning price decreases by 21.1%, and the shoulder and morning are complements with a cross elasticity of 0.10; thus the shoulder load decreases by 2.1%. + +- **Overall, when we sum the results of both effects, the shoulder usage increases by about 3.4%** due to the changes in prices of all four time periods and the complementarity/substitutability pattern between periods.[^p23] This 3.4% is then multiplied by the shoulder load under the traditional rate structure to obtain the load under TOU. + +[^p23]: Alternatively, the calculation can be presented in the following way: % change in shoulder usage = Conservation Effect + Shifting Effect = (% change in price of shoulder period × own-elasticity in shoulder period) + (% change in price of night period × cross-elasticity of shoulder and night + change in price of evening period × cross-elasticity of shoulder and evening + change in price of morning period × cross-elasticity of shoulder and morning). + +The identical process is applied to the other three periods of the day. Overall, the results from the table show that TOU introduction is associated with a decrease in load by about 6.5% (from 371,000 MWh under the traditional rate structure to 347,000 MWh under TOU). Revenues in response to TOU introduction decrease from $130 million to $126 million, or by about 3%. In Step 7 we discuss how to cover the gap in revenues under a TOU regime and under a traditional rate regime. + +In this paper we estimate only the effect that TOU rates have on consumer demand, and utility's loads and revenues. However, the introduction of dynamic rates has other effects as well. For example, the **Pricing Impact Simulation Model (PRISM) Suite** developed by the Brattle Group not only simulates customer response to dynamic pricing, but also estimates customers' and utilities' benefits. The models help to assess the following benefits: capacity cost savings, energy cost savings (lower energy generation costs and avoided wholesale power purchases), transmission cost savings, and distribution cost savings. + +The PRISM models are based on the data from 2003-2005 California Statewide Pricing Pilot. However, the models can help to evaluate the effects of dynamic rates programs of utilities in other parts of the country as well. The models allow adjusting for different climates, load shapes, and socio-demographic conditions.[^p24] + +[^p24]: For more details see Faruqui, A, and Wood, L. (2008). + +--- + +## VI. Step 6 – Conduct Sensitivity Analysis + +The review of existing elasticity studies reveals that it is virtually impossible to pinpoint demand elasticity. Since the results of our calculations depend on the assumptions about demand elasticity, it is important to perform a sensitivity analysis. Sensitivity analysis is a procedure that determines how sensitive the analysis's outcomes are to changes in assumptions. If a small change in assumptions results in relatively large changes in the outcomes, the outcomes are said to be sensitive to that assumption. + +The literature allows us to identify the range of reasonable elasticity estimates: from -0.12 to -0.35.[^p25] Higher values of elasticity reflect a larger customer response. An application of the lower and upper bounds of the range provides us with the range of loads and revenues we can expect from the introduction of TOU pricing. + +[^p25]: The values of demand elasticity for customers of actual utilities are expected to lie in this range. + +The elasticity matrix from Table 3 is adjusted for the lower and upper ends of elasticity estimates. The values of elasticities in Table 5 represent a lower end of the elasticity range, thus reflecting a smaller consumer response. The higher values of elasticities in Table 6 reflect the upper end of the elasticity range and a larger customer response. + +### Table 5. Elasticity Matrix for Sensitivity Analysis (lower bound — 0.12) + +| | Morning | Midday | Evening | Economy | +| ----------- | --------- | --------- | ---------- | ---------- | +| **Morning** | **-0.12** | -0.048 | -0.024 | 0.072 | +| **Midday** | -0.048 | **-0.12** | -0.024 | 0.024 | +| **Evening** | -0.048 | -0.024 | **-0.096** | 0.024 | +| **Night** | 0.072 | 0.024 | 0.072 | **-0.072** | + +### Table 6. Elasticity Matrix for Sensitivity Analysis (upper bound — 0.35) + +| | Morning | Midday | Evening | Economy | +| ---------------------- | --------- | --------- | --------- | --------- | +| **Morning** | **-0.35** | -0.14 | -0.07 | 0.21 | +| **Midday** | -0.14 | **-0.35** | -0.07 | 0.07 | +| **Evening** | -0.14 | -0.07 | **-0.28** | 0.07 | +| **Economy (off peak)** | 0.21 | 0.07 | 0.21 | **-0.21** | + +Next, we perform the procedure described in Step 5, applying different values of elasticities from the above tables. Table 7 presents the results of the sensitivity analysis that reflect changes in loads and revenues under new assumptions. + +### Table 7. Results of Sensitivity Analysis + +| | Elasticities from Table 3 | Lower-Bound Elasticities from Table 5 | Upper-Bound Elasticities from Table 6 | +| ----------------- | ------------------------- | ------------------------------------- | ------------------------------------- | +| Change in revenue | -6.5% | -3% | -9% | +| Change in load | -3% | 2% | -7% | + +The sensitivity analysis indicates that assumptions about the values of elasticities do not critically affect results. As we can see, even under upper-bound elasticities that assume large response from customers, changes in both revenues and loads due to the introduction of TOU pricing regime do not exceed 10%. Under lower-end estimates, revenues decrease by 3% and loads increase by 2%; under upper-end values, revenues drop by 9% and loads decrease by 7%. This is not a surprise, given that demand for electricity is not very elastic (consumers do not change their behavior much in response to changes in prices), as indicated by the small values of demand elasticity. If consumers do not adjust their patterns of consumption significantly, we cannot expect dramatic changes in sales. + +In this report we apply the values of elasticities for the short run. Most of the TOU experiments and programs were in place for only a short period of time; thus there is not much evidence regarding the long-term effects of TOU schedules. Two studies that examined the long-run effects of the Duke Power TOU program, which was in place for eight years, came to opposing conclusions regarding residential and non-residential customers. Taylor and Schwarz (1990) studied the long-run response of residential customers. The study concludes that "customer response increases over time in a manner that enhances the ability of TOU rates to reduce system peak."[^p26] Tishler (1989) comes to the opposite conclusion for non-residential customers: "Almost all the firm's adjustments take place at the time that the time-of-use pricing is introduced, and only very few additional adjustments take place in the long run."[^p27] + +[^p26]: Taylor & Schwarz (1990), p. 431. + +[^p27]: Tishler (1989), p. 69. + +Neither of the studies provides estimates of own and cross elasticities applicable for our analysis. King and Chatterjee (2003) provide a range of long-run estimates for residential customers from 34 non-TOU studies conducted before 1984. The values vary from -0.6 at the lower end of the range to -1.2 at the upper end. These values can be applied if the regulator is interested in estimating the long-run effects of a TOU program. However, these elasticity values merit careful consideration. First of all, they come from non-TOU studies, and non-TOU studies in general show higher elasticities than TOU studies. Second, the estimates come from studies conducted before 1984; more recent studies indicate lower elasticities, either due to changes in consumers' behavior over time or due to improved estimation techniques. + +--- + +## VII. Step 7 – Finalize TOU Pricing + +Equipped with the information from Steps 5 and 6, policymakers can make a decision about the feasibility of, and possible benefits from, TOU introduction. The decision should take into account (a) whether TOU pricing will induce enough response from customers to meet the utility's goal of peak-load reduction, and (b) how large the gap will be between TOU revenues and the utility's revenue requirements. If the decision is to proceed with TOU implementation, then the policymakers can use information from the previous two steps to adjust TOU rates upwards to cover the utility's costs and meet revenue requirements. Rates should increase to compensate for potential under-recovery of fixed costs (and thus earnings) due to changed consumption. Where consumption changes reduce costs as much as revenues, no adjustment is needed. + +As suggested in Step 4, the adjustment can be done by either proportionally increasing rates based on marginal costs or adding fixed charges. Rates should only be increased to reach the revenue requirement. Reductions in consumption could reduce the revenue requirement by lessening fuel or maintenance costs. If a 10% reduction in consumption would cause a 10% decrease in costs (fuel and maintenance), such an upward adjustment is not necessary. + +In our example, the costs did not fall as a result of reduced consumption. The marginal-cost-based rates for Utility X from Table 1 are 10.8 cents for off-peak hours, 21.0 cents for shoulder hours, and 42.4 cents for peak hours. In order to minimize the gap between revenues under traditional flat rates and TOU rates, rates in all TOU periods can be multiplied, for example by 1.0505 times. This method covers the gap while keeping the ratio intact (it is still about 1:2:4).[^p28] + +[^p28]: The difference is now $142.00 instead of -$376,512.00. + +--- + +## VIII. Conclusions + +This supplemental piece provides step-by-step guidance for the designing of TOU pricing. A simplified example of a hypothetical Utility X helps to illustrate the main steps of the process. + +--- + +## References + +1. Caves, D., Christensen, L. (1983). Time-of-use rates for residential electric service: Results from the Wisconsin Experiment. _Public Utilities Fortnightly_, 111. + +2. Conway, B.J. (1982). Time-differentiated accounting cost and short run marginal cost study. Prepared for Empire District Electric Company, December 1982. + +3. EPRI White Paper (2008). Price Elasticity of Demand for Electricity: A Primer and Synthesis, January 2008. + +4. Faruqui, A., and George, S. (2005). Quantifying customer response to dynamic pricing. _The Electricity Journal_, 18, May 2005. + +5. Faruqui, A., and George, S. (2002). The value of dynamic pricing in mass markets. _The Electricity Journal_, 15:6, July 2002. + +6. Faruqui, A, and Wood, L (2008). Quantifying the Benefits of Dynamic Pricing in the Mass Market. Prepared for Edison Electric Institute. January 2008. + +7. Hopper, N., Charles Goldman, Lawrence Berkeley National Laboratory, Bernie Neenan, Neenan Associates (A Utilipoint Company). (2006). Not all large customers are made alike: Disaggregating response to default-service day-ahead market pricing. August 2006. [LBNL Report](http://drrc.lbl.gov/pubs/59629.pdf) + +8. Rosenberg, E. (2007). Assessing wireless and broadband substitution in local telephone markets. National Regulatory Research Institute. + +9. Taylor & Schwarz. (1990). The long-run effects of a time-of-use demand charges. _RAND Journal of Economics_, 21. + +10. Tishler. (1989). The response of large firms to different schemes of time-of-use pricing when the production function is quadratic. _The Energy Journal_, 10, 1989. + +11. U.S. Department of Energy (2006). Benefits of demand response in electricity markets and recommendations for achieving them. A Report to the United States Congress Pursuant to Section 1252 of the Energy Policy Act of 2005. February 2006. [DOE Report](http://eetd.lbl.gov/ea/EMS/reports/congress-1252d.pdf) + +--- + +## Appendix A: Data Request + +Please provide: + +1. The average 24-hour load profile for each month in the last five calendar years for each type of the utility's customers +2. The average 24-hour marginal electricity costs in the last five calendar years based on: + - the heat rates/fuel costs of the last units dispatched if a utility produces electricity itself; or + - the cost of electricity purchased under bilateral contracts[^a1] +3. Currently effective rate schedules + +[^a1]: This is a data request for utilities that operate in states without organized markets. For utilities that operate in organized markets, the data on marginal costs are locational marginal prices (LMP) that can be obtained from the appropriate RTO's website. If the data are not readily available from the website, the data request should be sent to the RTO. + +--- + +## Appendix B: Data for Utility X + +| Hour | Marg cost on average day (¢/kWh) | Marg cost on min peak day (¢/kWh) | Marg cost on max peak day (¢/kWh) | Average load (MWh) | +| ---- | -------------------------------- | --------------------------------- | --------------------------------- | ------------------ | +| 1 | 10.12 | 9.2092 | 11.638 | 5,060 | +| 2 | 10.12 | 9.614 | 11.9416 | 5,060 | +| 3 | 10.12 | 9.5128 | 11.8404 | 5,060 | +| 4 | 10.12 | 9.8164 | 11.9416 | 5,060 | +| 5 | 39.468 | 36.31056 | 44.99352 | 19,734 | +| 6 | 40.48 | 37.6464 | 45.7424 | 20,240 | +| 7 | 45.54 | 45.0846 | 52.371 | 22,770 | +| 8 | 45.54 | 43.7184 | 51.0048 | 22,770 | +| 9 | 40.48 | 39.6704 | 47.3616 | 20,240 | +| 10 | 45.54 | 42.8076 | 54.1926 | 22,770 | +| 11 | 32.384 | 31.41248 | 35.94624 | 16,192 | +| 12 | 22.876 | 21.27468 | 26.53616 | 11,438 | +| 13 | 20.45 | 19.8365 | 23.722 | 10,225 | +| 14 | 20.24 | 18.4184 | 23.6808 | 10,120 | +| 15 | 20.24 | 19.4304 | 23.276 | 10,120 | +| 16 | 45.54 | 44.6292 | 51.9156 | 22,770 | +| 17 | 45.54 | 45.0846 | 51.4602 | 22,770 | +| 18 | 45.54 | 41.4414 | 53.7372 | 22,770 | +| 19 | 45.54 | 43.263 | 52.8264 | 22,770 | +| 20 | 45.702 | 43.87392 | 52.10028 | 22,851 | +| 21 | 42.656 | 39.24352 | 47.77472 | 21,328 | +| 22 | 33.396 | 31.05828 | 39.07332 | 16,698 | +| 23 | 14.168 | 13.17624 | 16.15152 | 7,084 | +| 24 | 10.12 | 9.3104 | 11.638 | 5,060 | + +| Period | Off-Peak | Shoulder | Peak | +| ------ | ---------------- | ---------------- | ---------------------------------------------------- | +| Hours | 11 p.m. - 4 a.m. | 12 p.m. - 3 p.m. | Morning: 5 a.m. - 11 a.m.; Evening: 4 p.m. - 10 p.m. | + +--- + +## Appendix C: Summary of the Review of Elasticity Studies + +### Elasticities of Residential Customers + +There are a number of studies of the demand elasticities of residential customers. Estimates found in these studies vary widely, from -0.076 to -2.01 for the short run and from -0.07 to -2.5 for the long run. The findings of most studies are located in the range of **-0.12 to -0.35**. + +Studies of time-of-use programs and experiments show that own-price elasticity in peak periods is usually higher than elasticity in off-peak periods. Voluntary TOU programs and experiments show a higher response than mandatory programs. + +In the long run, customers can change their appliance stock or install insulation; thus elasticities are expected to be higher than in the short run. Estimates for the long run, however, are less reliable, since most of the TOU projects were short-lived. + +### Elasticities of Non-residential Customers + +There is much less information about elasticities for non-residential customers than for residential customers. + +There is some evidence (from real-time pricing studies) that larger customers, especially in manufacturing, tend to be more price responsive, perhaps due to the fact that some of them own on-site generators. Hopper et al. (2006) examined the response of businesses in different sectors to the introduction of a day-ahead pricing program and found that "the manufacturing sector exhibited the highest elasticity, of 0.16…. The other sectors—commercial/retail, health care, and public works—are considerably less responsive, with average elasticities of 0.06, 0.04 and 0.02, respectively."[^c1] The results of the Braithwait and O'Sheasy (2002) study on real-time pricing indicated that "the most responsive customer segment was a group of very large industrial customers. This group exhibited a price elasticity of –0.18 to –0.28 across the range of reported prices ($0.15/kWh to $1.00/kWh), which was double the elasticity of any other group. The least responsive customer segments, consisting of smaller C&I customers that neither had onsite generation nor had previously participated in the utility's curtailable rate, exhibited price elasticities of -0.06 or lower at all price levels."[^c2] + +[^c1]: Hopper et al. (2006), p. 10. + +[^c2]: U.S. Department of Energy (2006), p. 88. + +The EPRI White Paper (2008) provides a review of the most recent studies of residential and non-residential customers' response to dynamic rates programs. From b69d06cef47e9bdaf2e7868465a9ed57061c3498 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 18:36:55 +0000 Subject: [PATCH 25/40] New rie_hp_nonhp_flat starting points... these are just copies of rie_hp_flat, which are properly precalibrated... these will be calibrated by cairo --- .../hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json | 2 +- .../ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json index 7aed204a..ba2d2b9d 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.0689577481656301, + "rate": 0.08892178419071482, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json index 79a671b5..9310eb58 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_nonhp_flat_supply.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.19101692500754738, + "rate": 0.3013395154610198, "adj": 0.0, "unit": "kWh" } From 7426f7e518a70ab12fdf4337490d18dce0c820e9 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 14:53:26 -0400 Subject: [PATCH 26/40] Rename compute-rev-requirements, uncomment runs 17-20 Rename recipe to compute-subclass-rev-requirements to match the underlying script. Uncomment runs 17-20 in run-all-parallel-tracks so the full 20-run batch executes. Made-with: Cursor --- .../code/orchestration/run_orchestration.md | 6 ++--- .../epmc_and_supply_allocation.md | 2 +- ...easonal_discount_plan_b_simplified_flat.md | 2 +- rate_design/hp_rates/Justfile | 26 +++++++++---------- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/context/code/orchestration/run_orchestration.md b/context/code/orchestration/run_orchestration.md index bfee0038..01e9efa0 100644 --- a/context/code/orchestration/run_orchestration.md +++ b/context/code/orchestration/run_orchestration.md @@ -56,7 +56,7 @@ all-pre (create-scenario-yamls, create-electric-tariff-maps-all, validate-confi │ ├─ run-1 (precalc default, delivery) │ │ - │ ├─ compute-rev-requirements ← computes differentiated rev requirement YAML from run-1 BAT output + │ ├─ compute-subclass-rev-requirements ← computes differentiated rev requirement YAML from run-1 BAT output │ │ (needed by runs 5, 6, 9, 10, 13, 14 -- all multi-tariff precalc runs) │ │ │ ├─ run-3 (default calibrated, delivery) ← copies calibrated tariff from run-1 @@ -265,7 +265,7 @@ RDP_BATCH=ny_20260305c_r1-8 just run-subset 1,2,5,6 ``` Delegates to `run-` recipes, so dependency logic (copy calibrated tariffs, -etc.) is preserved. Note: `compute-rev-requirements` is not auto-inserted; if +etc.) is preserved. Note: `compute-subclass-rev-requirements` is not auto-inserted; if the subset spans runs 1-2 and 3+, run it separately. ## Monitoring @@ -346,5 +346,5 @@ end-to-end wall time (see Parallel tracks below). pairs (6 waves of 2), each pair using half the available CPUs. This beats the sequential strategy when T4/T8 < 1.8 (see `cairo_parallelism_and_workers.md` for measured ratio). File conflicts: none — each wave pair writes to distinct tariff files and timestamped S3 -output directories. The `compute-rev-requirements` step remains serial between wave 1 and +output directories. The `compute-subclass-rev-requirements` step remains serial between wave 1 and wave 2 (it reads run-1 output and is fast; runs 5, 6, 9, and 10 all depend on it). diff --git a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md index cbc6c559..b3897823 100644 --- a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md +++ b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md @@ -190,7 +190,7 @@ The previous section explains the theory. This section tells you exactly how to 4. `**build_master_bat.py**`: detects available BAT metrics at runtime — gracefully handles missing BAT_peak and new BAT_epmc columns. 5. `**create_scenario_yamls.py**`: reads `residual_allocation_delivery` and `residual_allocation_supply` columns from the Google Sheet. -The key insight: all the subclass RR values (for every combination of allocation method) are computed once by `compute-rev-requirements` and written to a single YAML file. Each run then picks which delivery and supply method to use, via its scenario YAML. So testing is: run `compute-rev-requirements`, check the YAML, run the batch, check the outputs. +The key insight: all the subclass RR values (for every combination of allocation method) are computed once by `compute-subclass-rev-requirements` and written to a single YAML file. Each run then picks which delivery and supply method to use, via its scenario YAML. So testing is: run `compute-subclass-rev-requirements`, check the YAML, run the batch, check the outputs. ### Step-by-step: run and verify a NY batch diff --git a/context/plans/hp_seasonal_discount_plan_b_simplified_flat.md b/context/plans/hp_seasonal_discount_plan_b_simplified_flat.md index 3b5487bb..e085f182 100644 --- a/context/plans/hp_seasonal_discount_plan_b_simplified_flat.md +++ b/context/plans/hp_seasonal_discount_plan_b_simplified_flat.md @@ -153,7 +153,7 @@ The seasonal discount (runs 5-8), TOU (runs 9-12), and TOU flex (runs 13-16) sce - **TOU tariffs (runs 9-12):** Generated by `create-seasonal-tou-tariffs` during `all-pre` from marginal cost data. Never reads `hp_seasonal.json` or `seasonal_discount_rate_inputs.csv`. Uses the same `hp_vs_nonhp.yaml` RR but a completely independent tariff structure. Unaffected by Plan B. - **Flex tariffs (runs 13-16):** Copied from TOU tariff (`cp hp_seasonalTOU.json hp_seasonalTOU_flex.json`) and run with `elasticity: -0.1`. Same independence as TOU. Unaffected by Plan B. -- **Revenue requirement YAML:** Derived from runs 1-2 via `compute-rev-requirements`. Plan B doesn't change runs 1-2 or the RR computation. +- **Revenue requirement YAML:** Derived from runs 1-2 via `compute-subclass-rev-requirements`. Plan B doesn't change runs 1-2 or the RR computation. - **Non-HP tariff key:** Hardcoded in scenario YAMLs (NY: `nonhp_flat.json`, RI: `nonhp_default.json`). Plan B only touches the HP key. ## Adversarial review diff --git a/rate_design/hp_rates/Justfile b/rate_design/hp_rates/Justfile index f8fac5f3..6676dbd2 100644 --- a/rate_design/hp_rates/Justfile +++ b/rate_design/hp_rates/Justfile @@ -418,13 +418,13 @@ copy-calibrated-tariff-from-run run_dir output_dir="": "{{ state }}" \ "{{ output_dir }}" -compute-rev-requirements: +compute-subclass-rev-requirements: #!/usr/bin/env bash set -euo pipefail run1_dir=$(bash "{{ latest_output }}" "{{ path_scenario_config }}" 1) run2_dir=$(bash "{{ latest_output }}" "{{ path_scenario_config }}" 2) - echo ">> compute-rev-requirements: run-1 (delivery) → ${run1_dir}" >&2 - echo ">> compute-rev-requirements: run-2 (delivery+supply) → ${run2_dir}" >&2 + echo ">> compute-subclass-rev-requirements: run-1 (delivery) → ${run1_dir}" >&2 + echo ">> compute-subclass-rev-requirements: run-2 (delivery+supply) → ${run2_dir}" >&2 just compute-subclass-rr "${run1_dir}" "{{ path_scenario_config }}" \ "1" "has_hp" \ "{{ path_differentiated_rev_requirement }}" \ @@ -690,7 +690,7 @@ run-all-sequential: echo ">> run-all-sequential [${RDP_BATCH}]" >&2 just run-1 just run-2 - just compute-rev-requirements + just compute-subclass-rev-requirements just run-3 just run-4 just run-5 @@ -725,7 +725,7 @@ run-all-parallel-tracks: wait $PID1 || { echo "ERROR: run-1 failed" >&2; exit 1; } wait $PID2 || { echo "ERROR: run-2 failed" >&2; exit 1; } - just compute-rev-requirements + just compute-subclass-rev-requirements just run-3 & PID3=$! just run-4 & PID4=$! @@ -762,15 +762,15 @@ run-all-parallel-tracks: wait $PID15 || { echo "ERROR: run-15 failed" >&2; exit 1; } wait $PID16 || { echo "ERROR: run-16 failed" >&2; exit 1; } - # just run-17 & PID17=$! - # just run-18 & PID18=$! - # wait $PID17 || { echo "ERROR: run-17 failed" >&2; exit 1; } - # wait $PID18 || { echo "ERROR: run-18 failed" >&2; exit 1; } + just run-17 & PID17=$! + just run-18 & PID18=$! + wait $PID17 || { echo "ERROR: run-17 failed" >&2; exit 1; } + wait $PID18 || { echo "ERROR: run-18 failed" >&2; exit 1; } - # just run-19 & PID19=$! - # just run-20 & PID20=$! - # wait $PID19 || { echo "ERROR: run-19 failed" >&2; exit 1; } - # wait $PID20 || { echo "ERROR: run-20 failed" >&2; exit 1; } + just run-19 & PID19=$! + just run-20 & PID20=$! + wait $PID19 || { echo "ERROR: run-19 failed" >&2; exit 1; } + wait $PID20 || { echo "ERROR: run-20 failed" >&2; exit 1; } echo ">> run-all-parallel-tracks: all 20 runs complete" >&2 From 6b7b16a59f59fbaa6aa3ff8c174d517022b77ade Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 20:12:38 +0000 Subject: [PATCH 27/40] New percustomer/epmc/volumetric/passthrough subclass rr for RIE --- .../rev_requirement/rie_hp_vs_nonhp.yaml | 78 +++++++++++++------ 1 file changed, 55 insertions(+), 23 deletions(-) diff --git a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml index d2477b38..5ee9c67a 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml @@ -1,55 +1,66 @@ utility: rie group_col: has_hp -source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260326_r1-20_epmc/20260327_015312_ri_rie_run1_up00_precalc__default +source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260329_supply_passthrough/20260329_185643_ri_rie_run1_up00_precalc__default total_delivery_revenue_requirement: 542556587.55 total_delivery_and_supply_revenue_requirement: 955506957.66 subclass_revenue_requirements: delivery: percustomer: non-hp: 531018474.41656953 - hp: 11538113.133430587 + hp: 11538113.133430582 epmc: - non-hp: 528045651.5245807 + non-hp: 528045651.5245808 hp: 14510936.025419153 + volumetric: + non-hp: 518391179.89397407 + hp: 24165407.656025995 + passthrough: + non-hp: 518739812.78349507 + hp: 23816774.766508 supply: - passthrough: &id001 - non-hp: 394774670.835715 - hp: 18175699.274285078 - percustomer: *id001 + passthrough: + non-hp: 391820543.3235589 + hp: 21129826.786441203 + percustomer: + non-hp: 394774670.83571476 + hp: 18175699.27428508 + volumetric: + non-hp: 391562609.286075 + hp: 21387760.823924605 heating_type_breakdown: percustomer: delivered_fuels: delivery: 181332489.1256645 - supply: 135908283.90410492 - total: 317240773.0297694 + supply: 135908283.90410486 + total: 317240773.02976936 electrical_resistance: - delivery: 44312079.81038717 - supply: 66102143.52061749 - total: 110414223.33100466 + delivery: 44312079.810387164 + supply: 66102143.52061756 + total: 110414223.33100472 heat_pump: - delivery: 11538113.133430578 - supply: 18175699.274285085 + delivery: 11538113.133430585 + supply: 18175699.274285078 total: 29713812.407715663 natgas: - delivery: 294621861.52847034 - supply: 184890391.19549322 - total: 479512252.72396356 + delivery: 294621861.5284703 + supply: 184890391.1954931 + total: 479512252.7239634 other: delivery: 10752043.952047499 supply: 7873852.215499504 total: 18625896.167547002 epmc: delivered_fuels: - delivery: 207705127.43377423 + delivery: 207705127.43377417 supply: 117706857.03651214 - total: 325411984.47028637 + total: 325411984.4702863 electrical_resistance: - delivery: 35118464.54736017 - supply: 123390160.52429955 + delivery: 35118464.54736015 + supply: 123390160.52429956 total: 158508625.0716597 heat_pump: - delivery: 14510936.025419153 - supply: 31630893.341966927 + delivery: 14510936.025419157 + supply: 31630893.341966923 total: 46141829.36738608 natgas: delivery: 272906529.7775144 @@ -59,3 +70,24 @@ heating_type_breakdown: delivery: 12315529.765931955 supply: 6497253.052724594 total: 18812782.81865655 + volumetric: + delivered_fuels: + delivery: 188109558.81788254 + supply: 137632197.52143997 + total: 325741756.3393225 + electrical_resistance: + delivery: 84781682.33077732 + supply: 76396577.84717363 + total: 161178260.17795095 + heat_pump: + delivery: 24165407.656026 + supply: 21387760.82392461 + total: 45553168.47995061 + natgas: + delivery: 234571249.00894436 + supply: 169615047.52142903 + total: 404186296.5303734 + other: + delivery: 10928689.736369764 + supply: 7918786.396032965 + total: 18847476.13240273 From 913078343fa46f669a311d47a790049ab337639d Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 16:17:21 -0400 Subject: [PATCH 28/40] [agents] instruct agents not to accidentally render latex in cursor chat --- AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 017dd3eb..26d10fa1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -132,6 +132,8 @@ Match existing style: Ruff for formatting/lint, **ty** for type checking, dprint **LaTeX in Cursor chat:** When running inside Cursor, use `$$...$$` for display math and `$...$` for inline math. Cursor's chat renderer does not support `\[...\]` or `\(...\)` — the backslashes are consumed by the markdown parser, leaving bare brackets that the math renderer ignores. +**Currency dollar signs in chat:** Cursor's chat renderer treats `$...$` as LaTeX math delimiters. A bare `$` used for currency (e.g. `$1.4M`) will pair with the next `$` on the same or a later line, and everything in between renders as garbled math. **In chat output, always wrap ANY bare `$` in backticks** — not just obvious currency amounts like `$1.4M` and `$6/month`, but also unit notations like `$/kWh`, `$/month`, `$/day`, and any other string containing `$`. If it has a dollar sign and it's not LaTeX math, it needs backticks. This only applies to Cursor chat responses — `.md` files committed to the repo should use bare `$` for currency as usual. + ## Code Quality (required before every commit) - Run `just check` — no linter errors, no type errors, no warnings From fe9ecfc27f67a24bd2d36870206f78ba842367d3 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 16:17:50 -0400 Subject: [PATCH 29/40] [context] Figure out why passthrough isn't necessarily lower than volumetric residual allocation --- context/README.md | 13 +- .../epmc_and_supply_allocation.md | 18 +-- ...through_cost_allocation_and_rate_design.md | 130 ++++++++++++++++++ 3 files changed, 146 insertions(+), 15 deletions(-) create mode 100644 context/methods/bat_mc_residual/passthrough_cost_allocation_and_rate_design.md diff --git a/context/README.md b/context/README.md index d3afcf63..3caa621d 100644 --- a/context/README.md +++ b/context/README.md @@ -46,12 +46,13 @@ Documents that answer **"How do we justify and operationalize this?"** — conce BAT framework, residual allocation, and how they connect to the literature. -| File | Purpose | -| ------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| bat_reasoning_stress_test.md | Formal reconstruction and econ-seminar-style stress-test of the BAT framework: marginal costs, residual allocation, the three allocators, cross-subsidy definitions (BAT vs strict economic), standalone vs incremental cost | -| bat_lrmc_residual_allocation_methodology.md | Our MC methodology as a standalone method: LRMC decomposition into energy SRMC + capacity FLIC, formulas for each component (energy LBMP, gen capacity ICAP, bulk TX incremental benefit, dist MCOS incremental diluted), how each becomes 8760, residual definition and allocation, with citations showing alignment to the BAT paper, Borenstein, Pérez-Arriaga, Schittekatte & Meeus | -| epmc_and_supply_allocation.md | EPMC residual allocation and the delivery/supply split: why EPMC was added, the supply residual discovery (table of delivery vs supply residual % for all 8 utilities), the new YAML structure with separate delivery/supply blocks, four allocation methods (passthrough, volumetric, epmc, percustomer), the supply EPMC subtraction bug, current run configurations for RI and NY, and a self-contained mini-plan for an LLM to validate NY with exact commands and verification code | -| residual_allocation_lit_review_and_cairo.md | Residual allocation methods (volumetric, per-customer, peak, demand-based, historical consumption, Ramsey) from the cross-subsidization literature, mapped to CAIRO's three BAT allocators: formula, CAIRO status, literature survey by method, effect on solar/HP cross-subsidy. | +| File | Purpose | +| ---------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| bat_reasoning_stress_test.md | Formal reconstruction and econ-seminar-style stress-test of the BAT framework: marginal costs, residual allocation, the three allocators, cross-subsidy definitions (BAT vs strict economic), standalone vs incremental cost | +| bat_lrmc_residual_allocation_methodology.md | Our MC methodology as a standalone method: LRMC decomposition into energy SRMC + capacity FLIC, formulas for each component (energy LBMP, gen capacity ICAP, bulk TX incremental benefit, dist MCOS incremental diluted), how each becomes 8760, residual definition and allocation, with citations showing alignment to the BAT paper, Borenstein, Pérez-Arriaga, Schittekatte & Meeus | +| epmc_and_supply_allocation.md | EPMC residual allocation and the delivery/supply split: why EPMC was added, the supply residual discovery (table of delivery vs supply residual % for all 8 utilities), the new YAML structure with separate delivery/supply blocks, four allocation methods (passthrough, volumetric, epmc, percustomer), the supply EPMC subtraction bug, current run configurations for RI and NY, and a self-contained mini-plan for an LLM to validate NY with exact commands and verification code | +| passthrough_cost_allocation_and_rate_design.md | Why passthrough is a rate design outcome, not a cost allocation: worked numerical example showing the fixed charge reallocation effect, why volumetric can be above or below passthrough on delivery, why the comparison is clean on supply (no fixed charges), and proof that the underlying cost allocation behind a tariff is not uniquely recoverable | +| residual_allocation_lit_review_and_cairo.md | Residual allocation methods (volumetric, per-customer, peak, demand-based, historical consumption, Ramsey) from the cross-subsidization literature, mapped to CAIRO's three BAT allocators: formula, CAIRO status, literature survey by method, effect on solar/HP cross-subsidy. | ### methods/tou_and_rates/ diff --git a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md index b3897823..e4469722 100644 --- a/context/methods/bat_mc_residual/epmc_and_supply_allocation.md +++ b/context/methods/bat_mc_residual/epmc_and_supply_allocation.md @@ -72,26 +72,26 @@ The fix: `compute_subclass_rr` now precomputes every available allocation method ```yaml subclass_revenue_requirements: delivery: - passthrough: # no cross-subsidy correction + passthrough: # actual bills under default tariff (not a cost allocation) hp: 23816775 non-hp: 518739813 - percustomer: # corrects MC + residual cross-subsidy + percustomer: # EB by MC, residual by customer count hp: 11538113 non-hp: 531018474 - epmc: # allocates residual by MC share + epmc: # EB by MC, residual by MC share hp: 14510936 non-hp: 528045651 - volumetric: # allocates residual by kWh + volumetric: # EB by MC, residual by kWh hp: ... non-hp: ... supply: - passthrough: # same supply rate as default — no correction at all + passthrough: # actual supply bills under default tariff hp: 21129827 non-hp: 391820543 - percustomer: # corrects supply MC + residual cross-subsidy + percustomer: # EB by MC, residual by customer count hp: 18175699 non-hp: 394774670 - volumetric: # corrects supply MC cross-subsidy, not residual + volumetric: # EB by MC, residual by kWh hp: ... non-hp: ... # No EPMC: broken by the subtraction architecture. Volumetric gives @@ -111,9 +111,9 @@ The parser composes the total: `total_RR[subclass] = delivery[method_d][subclass Here's what each method does, concretely, when you apply it to a subclass: -**Passthrough** leaves the cross-subsidy intact on both the MC and the residual. HP's subclass RR equals their actual bills under the default flat rate. CAIRO then calibrates the HP tariff to recover those same bills — so the HP rate ends up being the same as the default rate. No correction at all. Use this when you don't want to touch that side of the bill (e.g., supply-side for a delivery-only rate design). This isn't technically a residual allocation method, since we don't actually calculate the residual and split it up somehow, but it's a hack to allow us to keep the supply (or delivery) part bill untouched by keeping the full cross-subsidy in that part of the subclass rr. +**Passthrough** sets HP's subclass RR equal to their actual bills under the default tariff. CAIRO then calibrates the HP tariff to recover those same bills — so the HP rate ends up being the same as the default rate. No correction at all. Use this when you don't want to touch that side of the bill (e.g., supply-side for a delivery-only rate design). This isn't technically a residual allocation method — it's the output of the existing rate design, not a decomposition into MC and residual. The underlying cost allocation that produced the current tariff is unknown and not uniquely recoverable (see `passthrough_cost_allocation_and_rate_design.md` for the full analysis). -**Volumetric** fixes the MC cross-subsidy but leaves the residual allocated by kWh (same as the flat rate). HP's subclass RR = HP's economic burden + (HP kWh share × total residual). If HP's average MC per kWh is below the system average (true on supply side, where capacity peaks in summer and HP load peaks in winter), HP gets a lower subclass RR than passthrough. If HP's average MC per kWh is above average (true on delivery side in summer-peaking systems), HP gets a higher subclass RR. This is not one of the residual allocation methods identified in NARUC's Electric Utility Cost Alloation Manual, because it isn't commonly applied to customer class allocation, but it is de-facto being apply to customer subclass allocation, so it's good to be able to represent it. +**Volumetric** allocates EB by actual MC (cost causality) and allocates the residual by kWh share. HP's subclass RR = HP's economic burden + (HP kWh share × total residual). Whether volumetric gives HP a higher or lower subclass RR than passthrough depends on two competing effects: (1) the MC correction (helps HP if their MC/kWh is below average, hurts if above), and (2) on delivery, the fixed charge reallocation (volumetric absorbs fixed charge revenue into the residual and splits it by kWh, which hurts HP when HP has more kWh/customer than average). On supply, where there are no fixed charges, only the MC correction matters. See `passthrough_cost_allocation_and_rate_design.md` for a worked example. This is not one of the residual allocation methods identified in NARUC's Electric Utility Cost Allocation Manual, because it isn't commonly applied to customer class allocation, but it is de facto being applied to customer subclass allocation, so it's good to be able to represent it. **EPMC** fixes the MC cross-subsidy and allocates the residual proportional to each subclass's share of total MC. This usually gives HP a lower total residual allocation than volumetric (because HP's MC share is typically less than their kWh share on supply, and the reverse on delivery). On delivery, where residual is 87–98% of costs, the allocation method matters enormously. On supply, where residual is 15–36%, the difference between EPMC and volumetric is small. This is known as equi-proportional residual allocation in NARUC's Electric Utility Cost Alloation Manual (NARUC, 1992, p. 160). diff --git a/context/methods/bat_mc_residual/passthrough_cost_allocation_and_rate_design.md b/context/methods/bat_mc_residual/passthrough_cost_allocation_and_rate_design.md new file mode 100644 index 00000000..283c4c92 --- /dev/null +++ b/context/methods/bat_mc_residual/passthrough_cost_allocation_and_rate_design.md @@ -0,0 +1,130 @@ +# Passthrough is not a cost allocation — it's a rate design + +This document works through a puzzle that arises when comparing passthrough to the other residual allocation methods (volumetric, EPMC, per-customer). The punchline: passthrough isn't a cost allocation method at all. It's the output of a rate design, and the underlying cost allocation that produced it is unknown. + +## The puzzle + +We have four "allocation methods" for splitting the revenue requirement between HP and non-HP: + +- **Per-customer**: HP_EB + (N_HP / N_total) × Residual +- **EPMC**: HP_EB + (HP_EB / total_EB) × Residual +- **Volumetric**: HP_EB + (HP_kWh / total_kWh) × Residual +- **Passthrough**: HP's actual bills under the default tariff + +The first three follow the same pattern: allocate EB by cost causality (actual MCs), allocate residual by some share metric. The fourth is different — it's just "whatever HP pays today." + +A natural expectation: volumetric should sit between passthrough and per-customer, because it allocates EB by MC (correcting the cross-subsidy on the MC portion) but leaves residual allocated by kWh (same as a flat rate). Since passthrough doesn't correct the MC cross-subsidy at all, and per-customer corrects both MC and residual, volumetric should be in the middle. + +For RIE delivery, the actual numbers are: + +| Method | HP subclass RR | +| ------------ | -------------- | +| Per-customer | $11.5M | +| EPMC | $14.5M | +| Passthrough | $23.8M | +| Volumetric | $24.2M | + +Volumetric is _above_ passthrough, not below. What's going on? + +## A numerical example + +**Setup:** 10 customers, 1 is HP, 9 are non-HP. + +| | HP (1 customer) | Non-HP (9 customers) | Total | +| -------------- | --------------- | -------------------- | ------ | +| Customers | 1 | 9 | 10 | +| kWh/year | 2,000 | 1,000 each = 9,000 | 11,000 | +| kWh share | 18.2% | 81.8% | 100% | +| Customer share | 10% | 90% | 100% | + +The default tariff has: + +- Fixed charge: $100/year per customer +- Flat volumetric rate: $0.818/kWh (calibrated so total revenue = RR) + +This gives: + +- Total delivery RR: $10,000 +- Fixed charge revenue: $1,000 (10 customers × $100) +- Volumetric revenue: $9,000 (11,000 kWh × $0.818) + +CAIRO's BAT also tells us: + +- Total EB (from MCs): $2,000 +- HP EB: $300 (HP avg MC/kWh = $0.15; system avg = $0.182 — HP is below average) +- Residual: $8,000 + +### Passthrough HP + +HP's actual bills: $100 (fixed) + 2,000 × $0.818 (vol) = $100 + $1,636 = **$1,736** + +### Volumetric HP + +HP_EB + (HP_kWh / total_kWh) × Residual = $300 + 18.2% × $8,000 = $300 + $1,455 = **$1,755** + +### Why is volumetric higher? + +Volumetric ($1,755) > passthrough ($1,736) by $19, even though HP's MC per kWh is below average. Two effects are at work: + +**Effect 1 — MC correction (helps HP, −$64).** Under passthrough, the flat volumetric rate charges every kWh the same, so HP's share of volumetric revenue equals their kWh share (18.2%). Under volumetric allocation, the EB portion uses HP's actual MCs. HP's actual EB ($300) is less than their kWh-proportional share of total EB (18.2% × $2,000 = $364), a saving of $64. This is because HP's load is winter-heavy and delivery MCs peak in summer. + +**Effect 2 — Fixed charge reallocation (hurts HP, +$82).** Under passthrough, HP pays $100 in fixed charges (1 customer × $100/year). Under volumetric allocation, the fixed charge revenue ($1,000) is embedded in the residual — because the residual is RR minus EB, and EB is purely from MCs, not from tariff structure. That $1,000 of fixed charge revenue sits inside the $8,000 residual and gets split by kWh. HP's kWh share of the fixed charge portion: 18.2% × $1,000 = $182. That's $82 more than the $100 they actually pay. HP is 18.2% of kWh but only 10% of customers, so splitting fixed charges by kWh assigns them more. + +**Net:** −$64 + $82 = **+$18** (≈ $19 with exact fractions). The fixed charge effect outweighs the MC correction. + +If the tariff had no fixed charges (pure volumetric), the MC correction would be the only effect, and volumetric would be $64 lower than passthrough. Fixed charges are what flip the result. + +### The supply side is different + +On the supply side, there are no fixed charges (verified across all 8 utilities — every supply charge is $/kWh). So for supply, passthrough is purely kWh-proportional, and the only difference between volumetric and passthrough is the MC correction. Whether volumetric is higher or lower than passthrough on supply depends entirely on whether HP's supply MC per kWh is above or below the system average. + +## Passthrough is a rate design, not a cost allocation + +The three BAT-based methods (per-customer, EPMC, volumetric) follow a two-step process: + +1. **Cost allocation**: split total EB and total residual between subclasses using some rule +2. **Rate design**: given the subclass RR from step 1, calibrate a tariff (fixed charge + vol rate) to recover it + +Passthrough skips step 1. It goes straight to the observed tariff and says: HP's subclass RR = their actual bills. This is the output of the utility's rate design, not of any explicit cost allocation. + +### Can we reverse-engineer the cost allocation? + +Given the observed tariff, we can uniquely determine HP's subclass RR ($1,736 in the example). But we cannot determine _how_ the utility arrived at that number. Any pair (α, β) satisfying: + +α × $2,000 (EB) + β × $8,000 (Residual) = $1,736 + +would produce the same RR. That's one equation with two unknowns — infinitely many solutions. For example: + +- α = β = 17.36% (uniform share of everything) +- α = 15% (MC-proportional), β = 17.95% +- α = 20%, β = 16.7% + +All produce the same $1,736 RR, the same fixed charges, and the same volumetric rate. From the observed tariff, these are indistinguishable. + +### Is it "kWh-proportional for both EB and residual"? + +No. If we allocated both EB and residual by kWh share, we'd get: + +18.2% × $2,000 + 18.2% × $8,000 = 18.2% × $10,000 = **$1,818** + +That's $82 higher than the actual $1,736. The difference is exactly the fixed charge effect: the tariff's fixed charges allocate $1,000 of the RR by customer count (HP gets 10%), while pure kWh-proportional would allocate it by kWh (HP gets 18.2%). The fixed charge gives HP a $82 benefit. + +### What the tariff implicitly does + +The default tariff is a hybrid: + +- Fixed charges: allocated by **customer count** (each customer pays the same $/month) +- Volumetric revenue: allocated by **kWh** (each kWh pays the same rate) +- MC and residual are lumped together — the tariff doesn't distinguish them + +This hybrid allocation doesn't correspond to any of our clean theoretical methods. It's an artifact of the tariff having both fixed and volumetric components. + +## Implications + +1. **Passthrough and volumetric are not directly comparable in the way the other methods are.** Per-customer, EPMC, and volumetric all use the same framework (separate MC and residual, allocate each by some rule). Passthrough uses a fundamentally different framework (actual bills under an observed tariff). Comparing them requires accounting for the fixed charge effect. + +2. **On delivery (where fixed charges exist), volumetric can be above or below passthrough.** It depends on whether the MC correction (which can go either way) or the fixed charge reallocation (which always hurts HP when HP has more kWh/customer than average) dominates. + +3. **On supply (where there are no fixed charges), the comparison is clean.** Volumetric vs passthrough is purely the MC correction. If HP's supply MC/kWh is above average, volumetric > passthrough; if below, volumetric < passthrough. + +4. **The "cross-subsidy" that passthrough preserves is not well-defined.** We can say passthrough preserves the status quo bills. But we can't say it preserves a specific MC cross-subsidy plus a specific residual cross-subsidy, because the decomposition into MC and residual is not observable from the tariff alone. From 0683338653ad6eec624c5f12b9413af43b797062 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Sun, 29 Mar 2026 20:22:33 +0000 Subject: [PATCH 30/40] New calibrated flat supply that fixes the EPMC-related bug --- .../config/tariffs/electric/rie_hp_flat_supply_calibrated.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json index 0b4dc1dc..b17ca6b1 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.3013395154610198, + "rate": 0.23081944795841336, "adj": 0.0, "unit": "kWh" } From 21aac722a33447dc15fc44f1111f9b8cb10aec0c Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 01:58:15 +0000 Subject: [PATCH 31/40] Move notes on residential charges in RI and NY to context/methods --- context/README.md | 4 ++-- .../ny_residential_charges_in_bat.md | 0 .../ri_residential_charges_in_bat.md | 17 +++++++++++------ .../ny/config/rev_requirement/top-ups/README.md | 4 ++-- .../ri/config/rev_requirement/top-ups/README.md | 2 +- utils/pre/rev_requirement/README.md | 2 +- utils/pre/rev_requirement/classify_charges.py | 4 ++-- 7 files changed, 19 insertions(+), 14 deletions(-) rename context/{code/data => methods/bat_mc_residual}/ny_residential_charges_in_bat.md (100%) rename context/{code/data => methods/bat_mc_residual}/ri_residential_charges_in_bat.md (98%) diff --git a/context/README.md b/context/README.md index d3afcf63..e00ead98 100644 --- a/context/README.md +++ b/context/README.md @@ -52,6 +52,8 @@ BAT framework, residual allocation, and how they connect to the literature. | bat_lrmc_residual_allocation_methodology.md | Our MC methodology as a standalone method: LRMC decomposition into energy SRMC + capacity FLIC, formulas for each component (energy LBMP, gen capacity ICAP, bulk TX incremental benefit, dist MCOS incremental diluted), how each becomes 8760, residual definition and allocation, with citations showing alignment to the BAT paper, Borenstein, Pérez-Arriaga, Schittekatte & Meeus | | epmc_and_supply_allocation.md | EPMC residual allocation and the delivery/supply split: why EPMC was added, the supply residual discovery (table of delivery vs supply residual % for all 8 utilities), the new YAML structure with separate delivery/supply blocks, four allocation methods (passthrough, volumetric, epmc, percustomer), the supply EPMC subtraction bug, current run configurations for RI and NY, and a self-contained mini-plan for an LLM to validate NY with exact commands and verification code | | residual_allocation_lit_review_and_cairo.md | Residual allocation methods (volumetric, per-customer, peak, demand-based, historical consumption, Ramsey) from the cross-subsidization literature, mapped to CAIRO's three BAT allocators: formula, CAIRO status, literature survey by method, effect on solar/HP cross-subsidy. | +| ny_residential_charges_in_bat.md | All NY residential electric charges (all 7 utilities) evaluated for BAT/bill calc: charge type taxonomy, master table, charge-by-charge analysis; Genability data quirks; discover → classify workflow for charge_decisions. | +| ri_residential_charges_in_bat.md | All RI residential electric charges (RIE A-16) evaluated for BAT/bill calc: charge type taxonomy, summary table, charge-by-charge analysis (base delivery, cost recon, program surcharges, sunk-cost, LMI recovery, LRS/ISO-NE supply decomposition, RES, GET). | ### methods/tou_and_rates/ @@ -127,8 +129,6 @@ ResStock, EIA, NYISO, ISO-NE: where data comes from, how to read and prepare it. | approximate_non_hp_load.md | MF highrise non-HP load approximation: nearest-neighbor RMSE, replace HVAC columns, update/sink, validation | | ny_lmi_discounts_genability_encoding.md | EAP/LMI encoding in NY Genability electric tariffs: which utilities encode EAP, tier/heating structure, amount comparison to lmi_discounts_in_ny.md | | tariff_rates_and_genability.md | Tariff/rate/rider/charge terminology, regulatory underpinnings (rate cases, rider proceedings, tariff books, leaf/revision versioning), Genability/Arcadia data model and APIs, and how to reconstruct historical monthly charges (API sequence, date-boundary subtleties, validation via Calculate API) | -| ny_residential_charges_in_bat.md | All NY residential electric charges (all 7 utilities) evaluated for BAT/bill calc: charge type taxonomy, master table, charge-by-charge analysis; Genability data quirks; discover → classify workflow for charge_decisions. | -| ri_residential_charges_in_bat.md | All RI residential electric charges (RIE A-16) evaluated for BAT/bill calc: charge type taxonomy, summary table, charge-by-charge analysis (base delivery, cost recon, program surcharges, sunk-cost, LMI recovery, LRS/ISO-NE supply decomposition, RES, GET). | | ny_genability_charge_fetch_map.md | Exhaustive charge-level table for NY Genability tariffs: tariffRateId, fetch_type, variableRateKey, master_charge, decision; used to fetch 2025 monthly rates from Arcadia API for top-up implementation. | | psegli_revenue_requirement_estimation.md | PSEG-LI revenue requirement estimation: why no rate case, TOU overcount problem, bill-proportional method from LIPA budget, charge classification, implementation via `estimate_psegli_rr.py`. | | tariff_generation_pipeline.md | Tariff generation pipeline: Genability → monthly_rates YAML → flat and default-structure URDB v7 JSONs. Covers fetch_monthly_rates.py, charge classification, monthly_rates YAML structure, create_flat_tariffs.py (top-down from RR), create_default_structure_tariffs.py (bottom-up from filed rates), BASE_TARIFF_PATTERN, Justfile wiring (all-pre), CAIRO calibration, and why flat vs default rates differ. | diff --git a/context/code/data/ny_residential_charges_in_bat.md b/context/methods/bat_mc_residual/ny_residential_charges_in_bat.md similarity index 100% rename from context/code/data/ny_residential_charges_in_bat.md rename to context/methods/bat_mc_residual/ny_residential_charges_in_bat.md diff --git a/context/code/data/ri_residential_charges_in_bat.md b/context/methods/bat_mc_residual/ri_residential_charges_in_bat.md similarity index 98% rename from context/code/data/ri_residential_charges_in_bat.md rename to context/methods/bat_mc_residual/ri_residential_charges_in_bat.md index c2c411da..a55858dc 100644 --- a/context/code/data/ri_residential_charges_in_bat.md +++ b/context/methods/bat_mc_residual/ri_residential_charges_in_bat.md @@ -21,6 +21,7 @@ Charges are classified into families based on their economic structure, not thei | **LMI cost recovery** | Recovery from non-LMI customers of low-income discount costs | Yes — but income transfer, not rate design | `exclude_eligibility` | | **Performance incentive** | PUC-approved performance incentive (e.g. EE program performance) | Yes in structure, negligible in magnitude | `exclude_negligible` | | **Redundant** | Minimum charge / bill floor; structurally redundant with customer charge | N/A | `exclude_redundant` | +| **Transmission service** | OATT-pass-through from ISONE | Yes | `add_to_drr` | | **Supply commodity** | LRS (Standard Offer) bundling ISO-NE wholesale energy, capacity, ancillary, admin | Mixed — see sub-components | `add_to_srr` | | **Merchant function** | LRS administrative cost adjustment (procurement, working capital) | Weak | `add_to_srr` | | **RES supply** | Per-MWh REC obligation (Renewable Energy Standard); cost scales with load | No | `add_to_srr` + MC 8760 | @@ -40,11 +41,11 @@ The Standard Offer Service (LRS) charge bundles several ISO-NE cost components. | **Operating & Maintenance Exp Charge** | Base delivery | $/kWh | Yes | — | Yes | O&M in rate case | `already_in_drr` | Yes—sub-tx + dx MCs | | **CapEx Factor Charge** | Base delivery | $/kWh | Yes | — | Yes | ISR plan capital recovery | `already_in_drr` | Yes—sub-tx + dx MCs | | **Pension Adjustment Factor** | Base delivery | $/kWh (credit) | Yes | — | Yes | Pension/benefit cost recovery | `already_in_drr` | No—residual | -| **Transmission Charge** | Base delivery | $/kWh | Yes | — | Yes | FERC/ISO-NE OATT pass-through | `already_in_drr` | Yes—transmission MCs | | **Minimum Charge** | Redundant | $/mo (floor) | Yes | — | N/A | Bill floor; equals customer charge | `exclude_redundant` | — | | **O&M Reconciliation Factor** | Cost recon | $/kWh | N/A (true-up) | — | No | Uniform $/kWh; true-up noise | `exclude_trueup` | — | | **CapEx Reconciliation Factor** | Cost recon | $/kWh | N/A (true-up) | — | No | Capital spending vs ISR forecast | `exclude_trueup` | — | | **RDM Adjustment Factor** | Revenue true-up | $/kWh | N/A (revenue) | — | No | Load forecast error; decoupling true-up | `exclude_trueup` | — | +| **Transmission Charge** | Transmission service | $/kWh | No | — | Yes | FERC/ISO-NE OATT pass-through | `add_to_drr` | Yes—transmission MCs | | **Energy Efficiency Programs Charge** | Program surcharge | $/kWh | **No** | Yes | **Yes** | Fixed EE budget (LCP) ÷ kWh | `add_to_drr` | No—residual | | **Net Metering Charge** | DER credit recovery | $/kWh | **No** | Yes | **Yes** | Fixed net metering cost recovery ÷ kWh | `add_to_drr` | No—residual | | **Long Term Contracting Charge** | Program surcharge | $/kWh | **No** | Yes (contracts) | **Yes** | Long-term renewable PPAs (e.g. offshore wind) ÷ kWh | `add_to_drr` | No—residual | @@ -93,11 +94,9 @@ These charges define the tariff structure that collects the delivery revenue req **Pension Adjustment Factor.** **$0.00339/kWh** as a **credit** (negative charge) in the Genability tariff; compliance filing showed ($0.00274)/kWh. Recovers pension and benefit costs for distribution employees; the credit reflects reconciliation of actual vs. forecast pension costs (e.g. Docket 25-10-EL). Part of labor-related cost recovery in the rate case. Treat as part of base delivery. -**Transmission Charge.** **$0.04773/kWh** (April 2025; tariff shows Base Transmission + Transmission Adjustment Factor + Transmission Uncollectible Factor). Recovers cost of high-voltage transmission (FERC-regulated, ISO-NE Open Access Transmission Tariff). Pass-through of Regional Network Service (RNS) and related OATT charges to distribution customers. Set by FERC/ISO-NE; RIE passes through. Same transmission marginal cost logic as in the BAT (transmission MC 8760). - **Minimum Charge.** **$6.00/month** — equals the customer charge. Bill floor for very low usage or net-generation months. Rarely binds for typical residential consumption. Structurally redundant with the customer charge. -**Decision.** All base delivery rates are `already_in_drr`. Minimum Charge is `exclude_redundant`. Sub-transmission and distribution marginal costs (MC 8760) apply to the volumetric delivery components; transmission MCs apply to the transmission charge. +**Decision.** All base delivery rates are `already_in_drr`. Minimum Charge is `exclude_redundant`. Sub-transmission and distribution marginal costs (MC 8760) apply to the volumetric delivery components. --- @@ -151,7 +150,13 @@ These charges define the tariff structure that collects the delivery revenue req ### Performance incentive -**Performance Incentive Factor.** Variable (often $0). Ties delivery rates to performance metrics (e.g. reliability, customer service, EE program performance). When non-zero, it is a small utility bonus recovered via $/kWh. Same as NY EAM: structurally a fixed pool ÷ kWh but negligible magnitude. **`exclude_negligible`.** +**Performance Incentive Factor.** Variable (often $0). Ties delivery rates to performance metrics (e.g. reliability, customer service, EE program performance). When non-zero, it is a small utility bonus recovered via $/kWh. Same as NY EAM: structurally a fixed pool ÷ kWh but negligible magnitude. `**exclude_negligible`.** + +--- + +### Transmission Service + +**Transmission Charge.** **$0.04773/kWh** (April 2025; tariff shows Base Transmission + Transmission Adjustment Factor + Transmission Uncollectible Factor). Recovers cost of high-voltage transmission (FERC-regulated, ISO-NE Open Access Transmission Tariff). Pass-through of Regional Network Service (RNS) and related OATT charges to distribution customers. Set by FERC/ISO-NE; RIE passes through. Same transmission marginal cost logic as in the BAT (transmission MC 8760). --- @@ -178,7 +183,7 @@ Rhode Island Energy procures default supply through **Last Resort Service (LRS)* ### Tax pass-through -**Gross Earnings Tax (GET).** **4.166667%** of charges (Genability: QUANTITY, PERCENTAGE). Rhode Island's public service corporation gross earnings tax (R.I. Gen. Laws Ch. 44-13) on electric (and gas) utilities, passed through to customers. Percentage of each customer's bill; no fixed pool. **`exclude_percentage`** — the pipeline cannot handle percentage-of-bill charges. +**Gross Earnings Tax (GET).** **4.166667%** of charges (Genability: QUANTITY, PERCENTAGE). Rhode Island's public service corporation gross earnings tax (R.I. Gen. Laws Ch. 44-13) on electric (and gas) utilities, passed through to customers. Percentage of each customer's bill; no fixed pool. `**exclude_percentage`** — the pipeline cannot handle percentage-of-bill charges. --- diff --git a/rate_design/hp_rates/ny/config/rev_requirement/top-ups/README.md b/rate_design/hp_rates/ny/config/rev_requirement/top-ups/README.md index b80d6c6b..8abc1b0e 100644 --- a/rate_design/hp_rates/ny/config/rev_requirement/top-ups/README.md +++ b/rate_design/hp_rates/ny/config/rev_requirement/top-ups/README.md @@ -42,7 +42,7 @@ belongs in the delivery revenue requirement, the supply revenue requirement, or be excluded from the BAT analysis. This classification was done through extensive manual research documented in -`context/code/data/ny_residential_charges_in_bat.md`. The result is one +`context/methods/bat_mc_residual/ny_residential_charges_in_bat.md`. The result is one `_charge_decisions.json` per utility in `charge_decisions/`, where every `tariffRateId` is mapped to a `decision`: @@ -154,6 +154,6 @@ To replicate this for a new state/utility: 1. **Add the utility to `utils/utility_codes.py`** with its EIA utility ID so the fetch script can resolve it to a Genability LSE. 2. **Add an entry to `tariffs_by_utility.yaml`** in the state's `rev_requirement/top-ups/` directory (use `default` to get the residential default tariff, or a specific `masterTariffId`). 3. **Fetch the base tariff**: run `fetch-genability-tariffs` with the appropriate effective date. -4. **Classify every charge** by reading through the tariff JSON and regulatory filings. This is unavoidable manual research — see `context/code/data/ny_residential_charges_in_bat.md` for the kind of analysis required. Document the research in a `context/code/data/_residential_charges_in_bat.md` file, then encode the decisions in a `_charge_decisions.json`. +4. **Classify every charge** by reading through the tariff JSON and regulatory filings. This is unavoidable manual research — see `context/methods/bat_mc_residual/ny_residential_charges_in_bat.md` for the kind of analysis required. Document the research in a `context/methods/bat_mc_residual/_residential_charges_in_bat.md` file, then encode the decisions in a `_charge_decisions.json`. 5. **Fetch the monthly rates**: run `fetch-monthly-rates` for the utility. 6. **Compute the revenue requirement**: run `compute-rr` to produce the topped-up `rev_requirement/.yaml`. diff --git a/rate_design/hp_rates/ri/config/rev_requirement/top-ups/README.md b/rate_design/hp_rates/ri/config/rev_requirement/top-ups/README.md index 542c3335..c898ecf9 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/top-ups/README.md +++ b/rate_design/hp_rates/ri/config/rev_requirement/top-ups/README.md @@ -41,7 +41,7 @@ belongs in the delivery revenue requirement, the supply revenue requirement, or be excluded from the BAT analysis. This classification was done through extensive manual research documented in -`context/code/data/ri_residential_charges_in_bat.md`. The result is +`context/methods/bat_mc_residual/ri_residential_charges_in_bat.md`. The result is `rie_charge_decisions.json` in `charge_decisions/`, where every `tariffRateId` is mapped to a `decision`: diff --git a/utils/pre/rev_requirement/README.md b/utils/pre/rev_requirement/README.md index 40c5d8ea..6af8cd84 100644 --- a/utils/pre/rev_requirement/README.md +++ b/utils/pre/rev_requirement/README.md @@ -140,7 +140,7 @@ in "Delivery & System" or "Power Supply". 2. Add an entry to `tariffs_by_utility.yaml` in the state's `top-ups/` directory. 3. Fetch the base tariff: `fetch-genability-tariffs`. 4. Classify every charge (manual research). Document in - `context/code/data/_residential_charges_in_bat.md`. + `context/methods/bat_mc_residual/_residential_charges_in_bat.md`. 5. Fetch monthly rates: `fetch-monthly-rates`. 6. Add a rate-case DRR entry in `delivery_rev_requirements_from_rate_cases.yaml`. 7. Run `compute-rr` to produce `rev_requirement/.yaml`. diff --git a/utils/pre/rev_requirement/classify_charges.py b/utils/pre/rev_requirement/classify_charges.py index b310507f..0a8bf2b5 100644 --- a/utils/pre/rev_requirement/classify_charges.py +++ b/utils/pre/rev_requirement/classify_charges.py @@ -3,8 +3,8 @@ Reads a *_discovered.json (output of ``fetch_monthly_rates.py --discover``), applies pattern-based classification rules derived from -``context/code/data/ny_residential_charges_in_bat.md`` and - ``context/code/data/ri_residential_charges_in_bat.md``, plus utility-specific +``context/methods/bat_mc_residual/ny_residential_charges_in_bat.md`` and + ``context/methods/bat_mc_residual/ri_residential_charges_in_bat.md``, plus utility-specific zonal/overlap dedup logic, then writes a hydrated charge_decisions.json. Supports all NY utilities (CenHud, ConEd, NiMo, NYSEG, O&R, PSEG-LI, RGE) From cdb20a70ee4336a46f19084c230c6d33bbfa08c7 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 01:58:49 +0000 Subject: [PATCH 32/40] Add Claude research note on RI's LRS charges and what costs they recover --- .../marginal_costs/ri_supply_cost_recovery.md | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 context/domain/marginal_costs/ri_supply_cost_recovery.md diff --git a/context/domain/marginal_costs/ri_supply_cost_recovery.md b/context/domain/marginal_costs/ri_supply_cost_recovery.md new file mode 100644 index 00000000..552762ee --- /dev/null +++ b/context/domain/marginal_costs/ri_supply_cost_recovery.md @@ -0,0 +1,97 @@ +# Rhode Island Energy's Last Resort Service Rate, Dissected + +Rhode Island Energy's residential Last Resort Service rate — the default electricity supply price for customers who haven't chosen a competitive supplier — comprises exactly **four volumetric components** totaling **14.770¢/kWh** for winter 2025–2026. Each component serves a distinct cost-recovery function, from wholesale power procurement to renewable energy compliance. The rate is governed by RIPUC Tariff No. 2096, changes on a seasonal schedule (April 1 for summer, October 1 for winter), and is designed as a pure pass-through: RIE earns no profit margin on any component.[^1] Understanding how these pieces fit together reveals a carefully layered system of forecasting, competitive procurement, and annual reconciliation. + +## The Base LRS Charge funds wholesale power through laddered solicitations + +The Base LRS Charge is the dominant component, set at **13.408¢/kWh** for winter 2025–2026 and **8.706¢/kWh** for summer 2025 — a seasonal swing that mirrors New England's wholesale electricity market, where winter natural gas constraints drive prices far above summer levels.[^2] + +RIE does **not** self-supply by purchasing directly from ISO-NE markets. Instead, it procures **Fixed-Price Full-Requirements (FPFR) load-following service** from wholesale suppliers through quarterly competitive solicitations (RFPs) managed by PPL Services Corporation.[^3] Under these contracts, winning suppliers take responsibility for energy costs (ISO-NE LMPs), ancillary services, and most other ISO-NE charges associated with serving the LRS load. The FPFR structure transfers wholesale price risk from ratepayers to suppliers, who price that risk into their bids. + +The procurement uses a **laddered approach** designed to mitigate price volatility. For residential and commercial customers, **90% of load** is procured through FPFR contracts across five separate solicitations, each covering 15–20% of load with terms ranging from 6 to 24 months. The remaining **10% is purchased on the ISO-NE spot market**, with estimated costs folded into the rate.[^3] This staggering ensures that no single market moment determines the entire rate. If a bid block receives zero offers, RIE defaults to spot market procurement; if only one bid arrives, the company consults the Division of Public Utilities within a two-hour window. + +**Capacity costs** were carved out of FPFR contracts beginning in April 2019 (PUC Order 23366, Docket 4809). RIE now separately estimates Forward Capacity Market payments and embeds them in the Base LRS Charge, a change made to reduce bid-price volatility caused by uncertain future capacity auction outcomes.[^4] + +The conversion from time-varying wholesale costs to a flat seasonal rate is straightforward arithmetic. RIE forecasts monthly LRS kWh by customer class, multiplies each month's estimated supply price by projected volume, sums the six months' costs, and divides by total projected kWh.[^5] For summer 2026, this produced a residential base charge of **8.788¢/kWh** against a projected cost of approximately **$107.2 million** over the six-month period.[^6] The total annual revenue collected through the Base LRS Charge across all customer groups runs to **several hundred million dollars**, as indicated by reconciliation swings of $11–26 million in recent years. + +## The Adjustment Factor reconciles forecast errors annually + +The LRS Adjustment Factor (governed by RIPUC No. 2237) is a pure **reconciliation mechanism** — the true-up between what the Base LRS Charge collected and what wholesale supply actually cost. Four categories of mismatch drive the balance: differences between forecasted and actual monthly kWh distribution, estimated versus actual line losses, estimated versus actual spot market prices, and billing-cycle timing differences.[^7] + +The factor is recalculated **once per year** as part of the Annual Retail Rate Filing submitted in February for effect April 1, applying for the subsequent 12-month period (April through March). The ending over/under-recovery balance (including interest) is divided by forecasted LRS kWh for the recovery period.[^8] + +For the current period, the residential Adjustment Factor is **−0.355¢/kWh** — a credit reflecting prior over-collection.[^2] Historical magnitudes vary dramatically: CY 2023 saw a **$24.1 million residential over-recovery** (resulting in a −0.777¢ credit the following year), while CY 2022 produced an **$18.3 million under-recovery** across all groups.[^8] These swings underscore the inherent difficulty of forecasting six months of wholesale costs into a flat rate. + +## Administrative costs recover working capital, bad debt, and procurement overhead + +The LRS Administrative Cost Factor currently stands at **0.256¢/kWh** for residential customers — a small but structurally interesting component.[^2] It consists of two sub-elements: a prospective estimate of upcoming administrative expenses and a reconciliation adjustment for prior-year over/under-collection. + +Three categories of cost flow through this factor: + +- **Cash working capital** (~$40.6 million in the 2025 filing): the return on funds RIE must advance to pay wholesale suppliers before collecting from retail customers. This is by far the largest dollar item, though the factor recovers only the carrying cost, not the principal.[^8] +- **Uncollectible expense**: bad debt from LRS customers who don't pay, calculated at a **1.30% uncollectible rate** in effect since September 2018.[^4] +- **Administrative expenses**: salaries and costs for employees managing RFPs, negotiating contracts, calculating rates, filing with the PUC, updating billing systems, creating environmental disclosure labels, and administering RES compliance.[^8] + +The total annual administrative cost budget across all customer groups was approximately **$7.65 million** in 2025, down from $9.77 million in 2023.[^8] The factor varies by customer class (commercial: 0.307¢; industrial: 0.148¢) because each class's share of total costs is divided by that class's projected kWh sales. + +**Why volumetric rather than a fixed charge?** No regulatory proceeding has specifically debated this design choice. The volumetric approach persists for practical reasons: the entire LRS supply charge is structured as a single per-kWh rate to enable direct comparison with competitive supplier offers. Embedding administrative costs in the volumetric commodity charge maintains this apples-to-apples comparability. The PUC treats all LRS costs as pure pass-throughs, and wholesale supply costs are inherently volume-dependent, so a volumetric structure follows naturally from the rate's origins in Rhode Island's late-1990s restructuring.[^1] + +## The RES Charge funds an accelerating renewable energy mandate + +The Renewable Energy Standard Charge has become the fastest-growing component of the LRS rate, rising from effectively **zero in 2018–2019** to **1.461¢/kWh** as of April 2025.[^2] This trajectory reflects Rhode Island's 2022 legislative acceleration of its RES to reach **100% renewable electricity by 2033** — one of the most aggressive timelines in the country.[^9] + +The charge recovers the cost of purchasing **Renewable Energy Certificates (RECs)** to satisfy RIE's statutory obligation under R.I.G.L. § 39-26. For compliance year 2025, the obligation is **34% of retail sales** (32% from "New" resources commissioned after 1997, plus 2% from "Existing" pre-1997 resources). The obligation escalates by 6–9.5 percentage points annually, reaching 100% in 2033.[^10] + +RIE procures RECs through three channels: **long-term power purchase agreements** with renewable generators (the primary source), the **Renewable Energy Growth feed-in tariff program** for distributed solar and small renewables, and annual **competitive RFP solicitations** for remaining needs.[^10] RIE has made **$0 in Alternative Compliance Payments** since at least 2013, indicating a well-supplied regional REC market.[^11] + +Recent New REC prices have been remarkably stable at approximately **$39–40/MWh**, near Connecticut's Class I ACP rate of $40/MWh, which effectively caps the regional price. Rhode Island's own ACP rate is much higher at **$80.59/MWh** (2023, CPI-adjusted from a $50 base), but the market clears well below this ceiling.[^11] The rising RES charge is driven almost entirely by the **increasing obligation percentage**, not by REC price inflation — a critical distinction for forecasting future costs. + +Total LRS RES compliance costs reached **$29.3 million in 2023** (covering 47.2% of statewide obligated load), composed almost entirely of New REC purchases ($28.9 million) with a small Existing REC component ($0.4 million).[^11] As the obligation percentage roughly triples from 2023 to 2033, annual costs will scale proportionally unless REC prices decline. + +**The RES charge is an embedded average cost**, not a marginal cost. RIE projects total annual REC procurement expenses, divides by forecasted LRS kWh, and adds a reconciliation adder for prior-year true-up. The volumetric structure is economically well-justified: the RES obligation is itself defined as a percentage of kWh sold, so each additional kilowatt-hour of consumption directly increases the number of RECs required. Cost causation aligns cleanly with volumetric recovery. + +## Rate component interactions and what the numbers reveal + +Viewing all four components together for winter 2025–2026 clarifies their relative weight: + +| Component | Rate (¢/kWh) | Share of total | +| --------------------- | ------------ | -------------- | +| Base LRS Charge | 13.408 | 90.8% | +| LRS Adjustment Factor | (0.355) | (2.4%) credit | +| LRS Admin Cost Factor | 0.256 | 1.7% | +| RES Charge | 1.461 | 9.9% | +| **Total** | **14.770** | **100%** | + +The Base LRS Charge overwhelmingly dominates. But the fastest-moving variable is the RES Charge, which has doubled in two years and will continue climbing as the 100%-by-2033 mandate accelerates. By contrast, the Administrative Cost Factor is remarkably stable, and the Adjustment Factor oscillates around zero over time by design. + +The procurement architecture deserves attention for its sophistication. The laddered FPFR solicitation approach, the separation of capacity costs from supplier bids, the 90/10 split between contracts and spot exposure, and the annual reconciliation cycle together create a system that balances price stability against cost accuracy. The six-month fixed-price commitment gives residential customers predictability, while the annual true-up prevents large cost mismatches from compounding indefinitely. + +## Conclusion + +Rhode Island Energy's LRS rate is not a single price but a layered mechanism reflecting four distinct cost streams, each with its own procurement logic and regulatory treatment. The Base LRS Charge — set through competitive full-requirements solicitations and weighted-average seasonal flattening — accounts for over 90% of the total and embeds wholesale energy, capacity, ancillary services, and spot market costs. The Adjustment Factor provides annual error correction. The Administrative Cost Factor, at roughly a quarter-cent per kWh, funds the institutional machinery needed to run the procurement program. And the RES Charge, now approaching 1.5¢/kWh and rising fast, finances Rhode Island's push toward 100% renewable electricity — a mandate that will likely make this component the primary driver of LRS rate increases over the next decade. The entire structure operates as a cost pass-through with no utility profit margin, regulated through a web of RIPUC dockets, annual filings, and quarterly solicitations documented in Tariff No. 2096 and the LRS Adjustment Provision (RIPUC No. 2237). + +--- + +## Sources + +[^1]: Rhode Island Energy, [Last Resort Service](https://rienergy.com/site/ways-to-save/rates-and-shopping/last-resort-service). RIE's overview of LRS as a pass-through default supply service. + +[^2]: RIPUC, [Compliance Filing — RIPUC Nos. 2095 & 2096, effective October 1, 2025](https://ripuc.ri.gov/sites/g/files/xkgbur841/files/2025-10/Compliance%20-%20RIPUC%20Nos.%202095%20&%202096%20-%20PUC%209-26-25.pdf). Tariff sheets showing the four LRS rate components for winter 2025–2026. + +[^3]: RIPUC, [Last Resort Service Regulatory Topic (August 2024)](https://ripuc.ri.gov/sites/g/files/xkgbur841/files/2024-08/Last%20Resort%20Service%20Regulatory%20Topic%20(08-2024).pdf). PUC staff overview of FPFR procurement mechanics, the 90/10 contract-to-spot split, laddered solicitation structure, and bidding protocols. + +[^4]: RIPUC, [Order 23770 — Docket 4930 (February 17, 2020)](https://ripuc.ri.gov/sites/g/files/xkgbur841/files/eventsactions/docket/4930-NGrid-Ord23770-2-17-20.pdf). PUC order addressing capacity cost carve-out from FPFR contracts and the 1.30% uncollectible rate. + +[^5]: Rhode Island Energy, [Supply Costs](https://www.rienergy.com/RI-Business/Rates/Supply-Costs). RIE's public explanation of how LRS rates are calculated from wholesale procurement costs. + +[^6]: RIPUC, [Docket 26-03-EL — LRS April 1, 2026 Rates (Public Filing, January 21, 2026)](https://ripuc.ri.gov/sites/g/files/xkgbur841/files/2026-01/26-03-EL-LRS%20April%201,%202026%20Rates%20-%20Public%20-%20PUC%201-21-2026.pdf). RIE's filing for summer 2026 LRS rates, including projected costs and the base charge calculation. + +[^7]: RIPUC, [Docket 25-04-EL — DPUC Position Memo (March 14, 2025)](https://ripuc.ri.gov/sites/g/files/xkgbur841/files/2025-03/25-04-EL%20RIE's%202025%20Retail%20Rate%20Filing%20-%20DPUC%20Position%20Memo%20(3-14-25).pdf). Division of Public Utilities review of the 2025 Annual Retail Rate Filing, discussing adjustment factor mechanics and administrative cost components. + +[^8]: RIPUC, [Compliance Filing — Dockets 25-03-EL, 25-04-EL, 25-05-EL, effective April 1, 2025](https://ripuc.ri.gov/sites/g/files/xkgbur841/files/2025-04/25-03-EL,%2025-04-EL,%2025-05-EL%20-%20Compliance%20-%20Effective%20April%201,%202025%20Rates%20-%20PUC%203-31-2025.pdf). Compliance tariff filing with detailed schedules for administrative costs, working capital, over/under-recovery balances, and the adjustment factor calculation. + +[^9]: Rhode Island Office of Energy Resources, [Governor McKee Signs Historic Legislation Requiring 100% of Rhode Island's Electricity to be Offset by Renewable Energy by 2033](https://energy.ri.gov/press-releases/governor-mckee-signs-historic-legislation-requiring-100-rhode-islands-electricity-be). Press release on the 2022 RES acceleration legislation. + +[^10]: Rhode Island Energy, [Supply Costs — Renewable Energy Standard](https://www.rienergy.com/RI-Business/Rates/Supply-Costs). RIE's description of RES procurement channels and the escalating obligation schedule. + +[^11]: Rhode Island RES, [2023 RES Annual Compliance Report](https://rhodeislandres.com/wp-content/uploads/2025/11/2023-RES-Annual-Compliance-Report.pdf). Detailed compliance data including total REC procurement costs ($29.3M in 2023), New and Existing REC prices, ACP rates, and historical compliance statistics. From 99adaf0ed41901061ecf626841dd4238a46aeca3 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 01:59:15 +0000 Subject: [PATCH 33/40] Install chromium so website-diff can parse plotly plots --- infra/user-data.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/infra/user-data.sh b/infra/user-data.sh index a8b7851b..7f58b5c8 100644 --- a/infra/user-data.sh +++ b/infra/user-data.sh @@ -50,6 +50,16 @@ apt-get install -y \ gh \ zsh +# Install Google Chrome (Ubuntu 22.04's chromium-browser is a snap stub that +# fails on EC2; Google's apt repo provides a real .deb). +# Needed by website-diff (Selenium) for Plotly chart pre-rendering. +curl -fsSL https://dl.google.com/linux/linux_signing_key.pub | + gpg --dearmor -o /usr/share/keyrings/google-chrome.gpg +echo "deb [arch=amd64 signed-by=/usr/share/keyrings/google-chrome.gpg] https://dl.google.com/linux/chrome/deb/ stable main" \ + >/etc/apt/sources.list.d/google-chrome.list +apt-get update +apt-get install -y google-chrome-stable + # Install AWS CLI v2 (official installer; apt awscli is v1) if ! command -v aws &>/dev/null || [ "$(aws --version 2>&1 | grep -o 'aws-cli/[0-9]*' | cut -d/ -f2)" = "1" ]; then echo "Installing AWS CLI v2..." From 1b5bff2acc4be42a88002c92ed50470d69b46d78 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 02:00:38 +0000 Subject: [PATCH 34/40] Add comments to rie monthly rates based on looking at RIE charges summary --- .../monthly_rates/rie_monthly_rates_2025.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/rate_design/hp_rates/ri/config/rev_requirement/top-ups/monthly_rates/rie_monthly_rates_2025.yaml b/rate_design/hp_rates/ri/config/rev_requirement/top-ups/monthly_rates/rie_monthly_rates_2025.yaml index 9930b112..8fdb9f6d 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/top-ups/monthly_rates/rie_monthly_rates_2025.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/top-ups/monthly_rates/rie_monthly_rates_2025.yaml @@ -35,7 +35,7 @@ add_to_drr: 2025-10: 6.0e-05 2025-11: 6.0e-05 2025-12: 6.0e-05 - net_metering_charge: + net_metering_charge: # In 2095 RIE rate summary. charge_unit: $/kWh monthly_rates: 2025-01: 0.01253 @@ -50,7 +50,7 @@ add_to_drr: 2025-10: 0.01457 2025-11: 0.01457 2025-12: 0.01457 - long_term_contracting_charge: + long_term_contracting_charge: # In 2095 RIE rate summary. charge_unit: $/kWh monthly_rates: 2025-01: 0.01092 @@ -65,7 +65,7 @@ add_to_drr: 2025-10: 0.00656 2025-11: 0.00656 2025-12: 0.00656 - re_growth_charge: + re_growth_charge: # In 2095 RIE rate summary. charge_unit: $/month monthly_rates: 2025-01: 4.02 @@ -80,7 +80,7 @@ add_to_drr: 2025-10: 3.22 2025-11: 3.22 2025-12: 3.22 # 3.22 month actual - liheap_enhancement_charge: + liheap_enhancement_charge: # In 2095 RIE rate summary. charge_unit: $/month monthly_rates: 2025-01: 0.79 @@ -95,7 +95,7 @@ add_to_drr: 2025-10: 0.79 2025-11: 0.79 2025-12: 0.79 # 0.78 actual - energy_efficiency_programs_charge: + energy_efficiency_programs_charge: # In 2095 RIE rate summary. charge_unit: $/kWh monthly_rates: 2025-01: 0.01098 @@ -110,7 +110,7 @@ add_to_drr: 2025-10: 0.01098 2025-11: 0.01098 2025-12: 0.01098 # .00195 actual - transmission_charge: + transmission_charge: # In 2095 RIE rate summary as Total Transmission Charge. charge_unit: $/kWh monthly_rates: 2025-01: 0.04161 @@ -176,7 +176,7 @@ already_in_drr: 2025-10: 6.0 2025-11: 6.0 2025-12: 6.0 - core_delivery_rate: + core_delivery_rate: # In 2095 RIE rate summary has volumetric Billing Distribution Charge, none of this is broken out. charge_unit: $/kWh monthly_rates: 2025-01: 0.0458 # .059 if next 3 charges included, .0534 actual, .0645 proposed @@ -251,7 +251,7 @@ excluded: decision: exclude_negligible last_resort_adjustment_factor: decision: exclude_trueup - transition_charge: + transition_charge: # In 2095 RIE rate summary. decision: exclude_trueup minimum: decision: exclude_redundant From f4714f1b0562506c470f6ec260826c05efa4ce9e Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 05:17:09 +0000 Subject: [PATCH 35/40] Fix bug in fetch monthly rates scripts that was counting credits as debits --- tests/test_fetch_monthly_rates.py | 76 +++++++++++++++++++ .../rev_requirement/fetch_monthly_rates.py | 27 +++++-- 2 files changed, 98 insertions(+), 5 deletions(-) create mode 100644 tests/test_fetch_monthly_rates.py diff --git a/tests/test_fetch_monthly_rates.py b/tests/test_fetch_monthly_rates.py new file mode 100644 index 00000000..79d74998 --- /dev/null +++ b/tests/test_fetch_monthly_rates.py @@ -0,0 +1,76 @@ +"""Tests for utils/pre/rev_requirement/fetch_monthly_rates.py.""" + +from __future__ import annotations + +from utils.pre.rev_requirement.fetch_monthly_rates import ( + _extract_bands, + _signed_rate, +) + + +class TestSignedRate: + """_signed_rate negates positive rateAmount when isCredit is true.""" + + def test_normal_positive(self): + assert _signed_rate({"rateAmount": 0.05}) == 0.05 + + def test_normal_negative(self): + assert _signed_rate({"rateAmount": -0.02}) == -0.02 + + def test_credit_positive_negated(self): + assert _signed_rate({"rateAmount": 0.00274, "isCredit": True}) == -0.00274 + + def test_credit_already_negative_unchanged(self): + assert _signed_rate({"rateAmount": -0.02, "isCredit": True}) == -0.02 + + def test_credit_zero_unchanged(self): + assert _signed_rate({"rateAmount": 0.0, "isCredit": True}) == 0.0 + + def test_credit_false_unchanged(self): + assert _signed_rate({"rateAmount": 0.05, "isCredit": False}) == 0.05 + + def test_none_amount(self): + assert _signed_rate({"rateAmount": None}) is None + + def test_missing_amount(self): + assert _signed_rate({}) is None + + +class TestExtractBands: + """_extract_bands applies _signed_rate to each band.""" + + def test_credit_band_negated(self): + rate = { + "rateBands": [ + { + "rateAmount": 0.00274, + "isCredit": True, + "consumptionUpperLimit": None, + "rateSequenceNumber": 1, + } + ] + } + bands = _extract_bands(rate) + assert len(bands) == 1 + assert bands[0]["rateAmount"] == -0.00274 + + def test_normal_band_unchanged(self): + rate = { + "rateBands": [ + { + "rateAmount": 0.05, + "isCredit": False, + "consumptionUpperLimit": 250, + "rateSequenceNumber": 1, + }, + { + "rateAmount": 0.07, + "consumptionUpperLimit": None, + "rateSequenceNumber": 2, + }, + ] + } + bands = _extract_bands(rate) + assert bands[0]["rateAmount"] == 0.05 + assert bands[1]["rateAmount"] == 0.07 + assert bands[0]["consumptionUpperLimit"] == 250 diff --git a/utils/pre/rev_requirement/fetch_monthly_rates.py b/utils/pre/rev_requirement/fetch_monthly_rates.py index f6887e86..28114d44 100644 --- a/utils/pre/rev_requirement/fetch_monthly_rates.py +++ b/utils/pre/rev_requirement/fetch_monthly_rates.py @@ -149,11 +149,24 @@ def _extract_tou(r: dict) -> dict | None: } +def _signed_rate(band: dict) -> float | None: + """Return the band's rateAmount with correct sign for credits. + + Genability uses two conventions for credits: either a natively negative + ``rateAmount``, or a positive ``rateAmount`` with ``isCredit: true``. + This helper normalises the second form so callers always get a signed value. + """ + amt = band.get("rateAmount") + if amt is not None and band.get("isCredit") and amt > 0: + return -amt + return amt + + def _extract_bands(r: dict) -> list[dict]: """Extract all rate bands from a Genability rate entry.""" return [ { - "rateAmount": b.get("rateAmount"), + "rateAmount": _signed_rate(b), "consumptionUpperLimit": b.get("consumptionUpperLimit"), "rateSequenceNumber": b.get("rateSequenceNumber"), } @@ -199,7 +212,7 @@ def _fetch_tariff_rates( resolved_rider_ids.add(r["riderTariffId"]) trid = r["tariffRateId"] bands = r.get("rateBands", []) - amt = bands[0].get("rateAmount") if bands else None + amt = _signed_rate(bands[0]) if bands else None if trid not in rate_map: rate_map[trid] = { "rateName": r["rateName"], @@ -292,7 +305,8 @@ def _discover_tariff_rates( if rider_tariff_id: resolved_rider_ids.add(rider_tariff_id) - first_amt = bands[0].get("rateAmount") if bands else None + first_amt = _signed_rate(bands[0]) if bands else None + any_credit = any(b.get("isCredit") for b in bands) source = "rider_resolved" if rider_tariff_id else "base_tariff" if trid not in discovered: @@ -307,10 +321,11 @@ def _discover_tariff_rates( "master_tariff_rate_id": r.get("masterTariffRateId"), "charge_unit": _derive_charge_unit(charge_type, charge_period), "sample_rate": first_amt, + "is_credit": any_credit, "is_tiered": len(bands) > 1, "rate_bands": [ { - "rate_amount": b.get("rateAmount"), + "rate_amount": _signed_rate(b), "upper_limit": b.get("consumptionUpperLimit"), } for b in bands @@ -352,7 +367,8 @@ def _discover_rider_rates( bands = r.get("rateBands", []) charge_type = r.get("chargeType") charge_period = r.get("chargePeriod") - first_amt = bands[0].get("rateAmount") if bands else None + first_amt = _signed_rate(bands[0]) if bands else None + any_credit = any(b.get("isCredit") for b in bands) entries[trid] = { "rate_name": r.get("rateName"), "charge_type": charge_type, @@ -364,6 +380,7 @@ def _discover_rider_rates( "master_tariff_rate_id": r.get("masterTariffRateId"), "charge_unit": _derive_charge_unit(charge_type, charge_period), "sample_rate": first_amt, + "is_credit": any_credit, "is_tiered": len(bands) > 1, "source": "rider_fetched", } From cb5d945451310b657ea542cbbc2588d041ee8681 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 05:18:10 +0000 Subject: [PATCH 36/40] Add comments to rev_requirement/.yml --- utils/pre/rev_requirement/compute_rr.py | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/utils/pre/rev_requirement/compute_rr.py b/utils/pre/rev_requirement/compute_rr.py index d64e8794..ea6c2d0d 100644 --- a/utils/pre/rev_requirement/compute_rr.py +++ b/utils/pre/rev_requirement/compute_rr.py @@ -609,10 +609,25 @@ def main() -> None: else None ) args.output.parent.mkdir(parents=True, exist_ok=True) + yaml_str = yaml.safe_dump( + out, default_flow_style=False, sort_keys=False, allow_unicode=True + ) + if args.use_resstock_loads: + inline_comments: dict[str, str] = { + "total_residential_kwh:": "# sum(resstock_monthly_kwh) * resstock_scale_factor", + "delivery_top_ups:": "# total_budgets are inflated by total_residential_kwh / eia_total_residential_kwh", + "resstock_scale_factor:": "# eia_customer_count / resstock_customer_count_for_utility", + } + lines = yaml_str.splitlines(keepends=True) + for i, line in enumerate(lines): + stripped = line.lstrip() + for key, comment in inline_comments.items(): + if stripped.startswith(key): + lines[i] = line.rstrip("\n") + f" {comment}\n" + break + yaml_str = "".join(lines) with open(args.output, "w") as f: - yaml.safe_dump( - out, f, default_flow_style=False, sort_keys=False, allow_unicode=True - ) + f.write(yaml_str) if __name__ == "__main__": From 739371c6da15ac7313508138dcf477bf31e20080 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 05:19:48 +0000 Subject: [PATCH 37/40] Fix rie rr from rate case from 241M to 171M, reclassify charges to add_to_drr, unexclude basically all charges --- .../ri_residential_charges_in_bat.md | 87 ++++-- ...very_rev_requirements_from_rate_cases.yaml | 2 +- .../ri/config/rev_requirement/rie.yaml | 60 +++- .../rie_charge_decisions.json | 20 +- .../monthly_rates/rie_monthly_rates_2025.yaml | 261 ++++++++++++------ 5 files changed, 294 insertions(+), 136 deletions(-) diff --git a/context/methods/bat_mc_residual/ri_residential_charges_in_bat.md b/context/methods/bat_mc_residual/ri_residential_charges_in_bat.md index a55858dc..072c8d8e 100644 --- a/context/methods/bat_mc_residual/ri_residential_charges_in_bat.md +++ b/context/methods/bat_mc_residual/ri_residential_charges_in_bat.md @@ -10,23 +10,24 @@ Every charge on a Rhode Island default residential electric bill — Rhode Islan Charges are classified into families based on their economic structure, not their tariff name. -| Type | What it is | Cross-subsidy? | Decision | -| -------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------- | -| **Base delivery** | Rates set in the rate case that collect the delivery revenue requirement | Yes (the BAT's core subject) | `already_in_drr` | -| **Cost reconciliation** | Uniform $/kWh true-ups of costs already embedded in base rates (delivery or supply) | No — shifts all bills equally | `exclude_trueup` | -| **Revenue true-up** | Revenue decoupling and temporary over/under-collection corrections | No — shifts all bills equally | `exclude_trueup` | -| **Program surcharge** | Fixed state-mandated program budgets recovered via uniform $/kWh or $/mo | Yes — fixed pool ÷ kWh (or ÷ customers) | `add_to_drr` | -| **Sunk-cost recovery** | Fixed debt, storm fund, or settlement pools recovered via $/kWh | Yes — fixed pool ÷ kWh | `add_to_drr` | -| **DER credit recovery** | Fixed net metering / RE Growth program costs recovered via uniform $/kWh | Yes — fixed pool ÷ kWh | `add_to_drr` | -| **LMI cost recovery** | Recovery from non-LMI customers of low-income discount costs | Yes — but income transfer, not rate design | `exclude_eligibility` | -| **Performance incentive** | PUC-approved performance incentive (e.g. EE program performance) | Yes in structure, negligible in magnitude | `exclude_negligible` | -| **Redundant** | Minimum charge / bill floor; structurally redundant with customer charge | N/A | `exclude_redundant` | -| **Transmission service** | OATT-pass-through from ISONE | Yes | `add_to_drr` | -| **Supply commodity** | LRS (Standard Offer) bundling ISO-NE wholesale energy, capacity, ancillary, admin | Mixed — see sub-components | `add_to_srr` | -| **Merchant function** | LRS administrative cost adjustment (procurement, working capital) | Weak | `add_to_srr` | -| **RES supply** | Per-MWh REC obligation (Renewable Energy Standard); cost scales with load | No | `add_to_srr` + MC 8760 | -| **Tax pass-through** | Gross Earnings Tax (% of bill); pipeline cannot handle %-of-bill charges | No | `exclude_percentage` | -| **Eligibility / optional** | A-60 low-income discount; $0 for default A-16 customer | N/A | `exclude_eligibility` | +| Type | What it is | Cross-subsidy? | Decision | +| ----------------------------- | ----------------------------------------------------------------------------------- | ------------------------------------------ | ---------------------- | +| **Base delivery** | Rates set in the rate case that collect the delivery revenue requirement | Yes (the BAT's core subject) | `already_in_drr` | +| **Rate adjustment provision** | Annual charges set by rate adjustment provisions filed outside the rate case | Yes — fixed cost ÷ kWh | `add_to_drr` | +| **Cost reconciliation** | Uniform $/kWh true-ups of costs already embedded in base rates (delivery or supply) | No — shifts all bills equally | `exclude_trueup` | +| **Revenue true-up** | Revenue decoupling and temporary over/under-collection corrections | No — shifts all bills equally | `exclude_trueup` | +| **Program surcharge** | Fixed state-mandated program budgets recovered via uniform $/kWh or $/mo | Yes — fixed pool ÷ kWh (or ÷ customers) | `add_to_drr` | +| **Sunk-cost recovery** | Fixed debt, storm fund, or settlement pools recovered via $/kWh | Yes — fixed pool ÷ kWh | `add_to_drr` | +| **DER credit recovery** | Fixed net metering / RE Growth program costs recovered via uniform $/kWh | Yes — fixed pool ÷ kWh | `add_to_drr` | +| **LMI cost recovery** | Recovery from non-LMI customers of low-income discount costs | Yes — but income transfer, not rate design | `exclude_eligibility` | +| **Performance incentive** | PUC-approved performance incentive (e.g. EE program performance) | Yes in structure, negligible in magnitude | `exclude_negligible` | +| **Redundant** | Minimum charge / bill floor; structurally redundant with customer charge | N/A | `exclude_redundant` | +| **Transmission service** | OATT-pass-through from ISONE | Yes | `add_to_drr` | +| **Supply commodity** | LRS (Standard Offer) bundling ISO-NE wholesale energy, capacity, ancillary, admin | Mixed — see sub-components | `add_to_srr` | +| **Merchant function** | LRS administrative cost adjustment (procurement, working capital) | Weak | `add_to_srr` | +| **RES supply** | Per-MWh REC obligation (Renewable Energy Standard); cost scales with load | No | `add_to_srr` + MC 8760 | +| **Tax pass-through** | Gross Earnings Tax (% of bill); pipeline cannot handle %-of-bill charges | No | `exclude_percentage` | +| **Eligibility / optional** | A-60 low-income discount; $0 for default A-16 customer | N/A | `exclude_eligibility` | --- @@ -38,9 +39,9 @@ The Standard Offer Service (LRS) charge bundles several ISO-NE cost components. | --------------------------------------------- | --------------------- | ---------------- | ------------- | -------------------- | ------------------- | ------------------------------------------------------------------------------ | --------------------- | ---------------------- | | **Customer Charge** | Base delivery | $/mo | Yes | — | Yes | Base rate; part of tariff | `already_in_drr` | No—residual | | **Distribution Charge** | Base delivery | $/kWh | Yes | — | Yes | Rate CAIRO calibrates | `already_in_drr` | Yes—sub-tx + dx MCs | -| **Operating & Maintenance Exp Charge** | Base delivery | $/kWh | Yes | — | Yes | O&M in rate case | `already_in_drr` | Yes—sub-tx + dx MCs | -| **CapEx Factor Charge** | Base delivery | $/kWh | Yes | — | Yes | ISR plan capital recovery | `already_in_drr` | Yes—sub-tx + dx MCs | -| **Pension Adjustment Factor** | Base delivery | $/kWh (credit) | Yes | — | Yes | Pension/benefit cost recovery | `already_in_drr` | No—residual | +| **Operating & Maintenance Exp Charge** | Rate adj. provision | $/kWh | **No** (ISR) | Yes (ISR provision) | **Yes** | ISR O&M; annual provision (§ 39-1-27.7.1) | `add_to_drr` | No—residual | +| **CapEx Factor Charge** | Rate adj. provision | $/kWh | **No** (ISR) | Yes (ISR provision) | **Yes** | ISR capital; annual provision (§ 39-1-27.7.1) | `add_to_drr` | No—residual | +| **Pension Adjustment Factor** | Rate adj. provision | $/kWh (credit) | **No** (PAM) | Yes (PAM provision) | **Yes** | Pension/PBOP reconciliation (FASB Topic 715) | `add_to_drr` | No—residual | | **Minimum Charge** | Redundant | $/mo (floor) | Yes | — | N/A | Bill floor; equals customer charge | `exclude_redundant` | — | | **O&M Reconciliation Factor** | Cost recon | $/kWh | N/A (true-up) | — | No | Uniform $/kWh; true-up noise | `exclude_trueup` | — | | **CapEx Reconciliation Factor** | Cost recon | $/kWh | N/A (true-up) | — | No | Capital spending vs ISR forecast | `exclude_trueup` | — | @@ -82,21 +83,45 @@ The same integration approach applies: **top up the revenue requirement** (deliv ### Base delivery rates -These charges define the tariff structure that collects the delivery revenue requirement. CAIRO should model all of them. +These are the only charges set in the rate case that collect the base delivery revenue requirement. CAIRO calibrates the volumetric distribution charge in precalc mode. **Customer Charge.** Rhode Island Energy's A-16 customer charge is **$6.00/month** (set in RI PUC rate cases). It covers account-level costs: metering, billing systems, customer service. One of the lower customer charges in the region. Fixed charges are load-shape-insensitive. -**Distribution Charge.** The main volumetric delivery rate: **$0.0458/kWh** (effective April 2025 per compliance filing). Recovers the cost of local distribution (wires, poles, transformers). Flat rate; applies to every kWh. This is the primary rate CAIRO calibrates in precalc mode. +**Distribution Charge.** The main volumetric delivery rate: **$0.0458/kWh** (effective April 2025 per compliance filing). Recovers the cost of local distribution (wires, poles, transformers). Flat rate; applies to every kWh. This is the primary rate CAIRO calibrates in precalc mode. Together with the Customer Charge, these are the only delivery charges set in the rate case and reflected in the base delivery revenue requirement. -**Operating & Maintenance Exp Charge.** **$0.00223/kWh.** Recovers day-to-day O&M for the distribution system. Set in the rate case; part of the delivery revenue requirement. Functionally part of base delivery; same marginal cost logic applies as for the distribution charge. +**Minimum Charge.** **$6.00/month** — equals the customer charge. Bill floor for very low usage or net-generation months. Rarely binds for typical residential consumption. Structurally redundant with the customer charge. + +**Decision.** Customer Charge and Distribution Charge are `already_in_drr`. Minimum Charge is `exclude_redundant`. Sub-transmission and distribution marginal costs (MC 8760) apply to the Distribution Charge. -**CapEx Factor Charge.** **$0.00832/kWh** (April 2025). Recovers capital expenditure for the distribution system under Rhode Island's **Infrastructure, Safety, and Reliability (ISR)** plan. RIE files annual Electric ISR Plans with the RI PUC; the plan covers capital investment, vegetation management O&M, and other reliability spending. The CapEx factor is the revenue requirement component for approved ISR capital. Functionally part of base delivery; same marginal costs apply. +--- -**Pension Adjustment Factor.** **$0.00339/kWh** as a **credit** (negative charge) in the Genability tariff; compliance filing showed ($0.00274)/kWh. Recovers pension and benefit costs for distribution employees; the credit reflects reconciliation of actual vs. forecast pension costs (e.g. Docket 25-10-EL). Part of labor-related cost recovery in the rate case. Treat as part of base delivery. +### Rate Adjustment Provisions -**Minimum Charge.** **$6.00/month** — equals the customer charge. Bill floor for very low usage or net-generation months. Rarely binds for typical residential consumption. Structurally redundant with the customer charge. +The charges below are billed as part of the delivery rate but are **not** set in the rate case. They are authorized by specific **Rate Adjustment Provisions** — formal, legally binding tariff provisions approved by the Rhode Island Public Utilities Commission (RIPUC). Many are explicitly mandated by Rhode Island General Laws to ensure the utility meets specific public policy, infrastructure, and social goals. By isolating these volatile, state-mandated costs into their own provisions, the utility can perform annual mathematical true-ups to adjust the billed rate up or down, ensuring it recovers exactly what was spent without filing a multi-year base rate case every time a budget fluctuates. + +Together with the base Distribution Charge and the charges classified under "Sunk-cost recovery" below (Storm Fund Replenishment and Arrearage Management), the Rate Adjustment Provision charges make up the total **billed delivery rate** — the volumetric $/kWh that appears on the customer's bill. Only the Customer Charge and Distribution Charge are set in the rate case and reflected in the base delivery revenue requirement. + +The Rate Adjustment Provisions that govern delivery-side charges on the A-16 bill are: + +1. **Infrastructure, Safety, and Reliability (ISR) Provision.** Governs the **CapEx Factor Charge**, CapEx Reconciliation Factor, **O&M Expense Charge**, and O&M Reconciliation Factor. Authorized under R.I. Gen. Laws § 39-1-27.7.1, which allows the utility to propose an annual spending plan to maintain grid safety and reliability without a full base rate case. The provision mathematically details how the utility must reconcile its actual capital and O&M spending against what it billed customers each year. + +2. **Pension Adjustment Mechanism (PAM) Provision.** Governs the **Pension Adjustment Factor (PAF)**. Allows the utility to reconcile actual pension and post-retirement benefit (PBOP) expenses — calculated strictly using FASB Topic 715 accounting rules — against the fixed allowance embedded in base rates. Pension costs fluctuate heavily with stock market performance and actuarial changes. -**Decision.** All base delivery rates are `already_in_drr`. Minimum Charge is `exclude_redundant`. Sub-transmission and distribution marginal costs (MC 8760) apply to the volumetric delivery components. +3. **Revenue Decoupling Mechanism (RDM) Provision.** Governs the RDM Adjustment Factor. Established pursuant to the "Decoupling Act" (R.I. Gen. Laws § 39-1-27.7.1). Severs the link between utility profits and volume of electricity sold, ensuring the utility is not financially penalized for promoting state-mandated energy efficiency and climate policy goals. + +4. **Storm Fund Replenishment Provision.** Governs the Storm Fund Replenishment Factor. Provides the legal mechanism for a uniform per-kWh charge to replenish the reserve fund for emergency storm restoration costs. + +5. **Residential Assistance Provision.** Governs both the Low-Income Discount Recovery Factor (LIDRF) and the Arrearage Management Adjustment Factor (AMAF). The LIDRF recovers the cost of 25% or 30% bill discounts for eligible low-income customers on the A-60 rate. The AMAF complies with R.I. Gen. Laws § 39-2-1(d)(2), which dictates the parameters under which the utility forgives past-due balances for qualifying low-income customers and recovers that debt across the broader customer base. + +Of these, the charges classified as `add_to_drr` in this section are the ISR O&M and CapEx factors and the Pension Adjustment Factor — the provision-based charges that recover fixed annual cost pools via $/kWh. The ISR and Pension _reconciliation_ factors are `exclude_trueup` (uniform true-ups; see "Cost reconciliation" below). The RDM is `exclude_trueup` (revenue decoupling). The Storm Fund and Arrearage charges are classified under "Sunk-cost recovery." The Residential Assistance charges are classified under "Program surcharges" (LIDRF → `exclude_eligibility`; AMAF → `add_to_drr`). + +**Operating & Maintenance Exp Charge.** **$0.00223/kWh.** Recovers day-to-day O&M for the distribution system under the **ISR Provision** (R.I. Gen. Laws § 39-1-27.7.1). The annual ISR plan sets O&M spending; the charge recovers approved costs via uniform $/kWh. Fixed annual provision amount ÷ kWh → HP cross-subsidy. **Add to d.r.r.** No MC 8760 — residual. + +**CapEx Factor Charge.** **$0.00832/kWh** (April 2025). Recovers capital expenditure for the distribution system under the **ISR Provision**. RIE files annual Electric ISR Plans covering capital investment, vegetation management O&M, and reliability spending. The CapEx factor is the revenue requirement component for approved ISR capital. Fixed annual provision amount ÷ kWh → HP cross-subsidy. **Add to d.r.r.** No MC 8760 — residual. + +**Pension Adjustment Factor.** **$0.00339/kWh** as a **credit** (negative charge) in the Genability tariff; compliance filing showed ($0.00274)/kWh. Reconciles actual pension and post-retirement benefit expenses (FASB Topic 715) against the fixed allowance in base rates under the **Pension Adjustment Mechanism Provision**. The credit reflects over-recovery in prior periods (e.g. Docket 25-10-EL). Fixed annual reconciliation ÷ kWh. **Add to d.r.r.** No MC 8760 — residual. + +**Decision.** `add_to_drr` for the ISR O&M and CapEx charges and the Pension Adjustment Factor. No MC 8760 — these are annual provision amounts allocated as residual. --- @@ -213,10 +238,12 @@ Rhode Island has one major electric distribution utility (RIE) and one default r - **Capacity:** ISO-NE uses a Forward Capacity Market (FCM) with annual auctions and reconfiguration auctions; capacity obligation is by peak load contribution. NYISO uses installed capacity (ICAP/UCAP) with locational requirements. In both cases, residential default service recovers capacity cost volumetrically → HP cross-subsidy when system is not winter-peaking. - **Supply procurement:** RI uses competitive LRS procurement (FPFR + spot); the utility retains capacity responsibility. NY utilities typically procure from NYISO and pass through via an MSC-like charge. Decomposition (energy vs capacity vs ancillary vs admin) is analogous for BAT and MC 8760. -### ISR plan and reconciliation +### Rate Adjustment Provisions and the billed delivery rate + +Rhode Island's billed delivery rate is composed of a base rate (Customer Charge + Distribution Charge, set in the rate case) plus a series of **Rate Adjustment Provisions** — formal, legally binding tariff provisions filed annually with RIPUC. These provisions allow annual true-ups of specific cost categories without a full rate case, ensuring the utility recovers exactly what was spent. The provisions are detailed in the "Rate Adjustment Provisions" charge-by-charge section above. -RI's **Infrastructure, Safety, and Reliability (ISR)** plan is an annual capital (and related O&M) plan filed with the PUC. The CapEx Factor Charge recovers approved ISR capital; the CapEx Reconciliation Factor true-ups actual vs. forecast. This is distinct from a single multi-year rate case but functionally part of base delivery for BAT purposes. +For BAT purposes, the base delivery rates are `already_in_drr` — CAIRO calibrates them. The provision-based charges that recover fixed cost pools via $/kWh (ISR O&M, ISR CapEx, Pension Adjustment Factor) are `add_to_drr` — topped up so the BAT can allocate them under the chosen residual allocator. The reconciliation factors from these same provisions (ISR O&M Recon, ISR CapEx Recon, RDM) are `exclude_trueup` — uniform true-ups that don't change cross-subsidy rankings. ### Delivery vs supply on the bill -Genability and the compliance filing separate **delivery** (customer charge, distribution, O&M, CapEx, pension, transmission, transition, EE, net metering, long-term contracting, RE Growth, LIHEAP, storm fund, arrearage, LMI recovery, performance, LRS adjustment) from **supply** (LRS base, LRS adjustment, LRS admin, RES). For BAT: delivery components either sit in d.r.r. (base + add-ons we top up) or are excluded (reconciliation, revenue true-up, LMI recovery, tax); supply sits in s.r.r. with RES and LRS admin added and LRS true-up excluded.gst +Genability and the compliance filing separate **delivery** (customer charge, distribution, O&M, CapEx, pension, transmission, transition, EE, net metering, long-term contracting, RE Growth, LIHEAP, storm fund, arrearage, LMI recovery, performance, LRS adjustment) from **supply** (LRS base, LRS adjustment, LRS admin, RES). For BAT: delivery components either sit in d.r.r. (base + add-ons we top up) or are excluded (reconciliation, revenue true-up, LMI recovery, tax); supply sits in s.r.r. with RES and LRS admin added and LRS true-up excluded. diff --git a/rate_design/hp_rates/ri/config/rev_requirement/delivery_rev_requirements_from_rate_cases.yaml b/rate_design/hp_rates/ri/config/rev_requirement/delivery_rev_requirements_from_rate_cases.yaml index 7db048ed..d968f576 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/delivery_rev_requirements_from_rate_cases.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/delivery_rev_requirements_from_rate_cases.yaml @@ -1,3 +1,3 @@ # Rate-case delivery revenue requirements (hard-coded from PUC filings). # One entry per utility. Used as input to compute_rr.py; do not overwrite with computed values. -rie: 241869601.0 +rie: 171228718 diff --git a/rate_design/hp_rates/ri/config/rev_requirement/rie.yaml b/rate_design/hp_rates/ri/config/rev_requirement/rie.yaml index 2d34a80e..3162f134 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/rie.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/rie.yaml @@ -1,14 +1,38 @@ utility: rie load_method: resstock -delivery_revenue_requirement_from_rate_case: 241869601.0 -delivery_revenue_requirement_topups: 300686986.55 +delivery_revenue_requirement_from_rate_case: 171228718.0 +delivery_revenue_requirement_topups: 330633113.36 supply_revenue_requirement_topups: 412950370.11 -total_delivery_revenue_requirement: 542556587.55 -total_delivery_and_supply_revenue_requirement: 955506957.66 -total_residential_kwh: 3173819718.1 +total_delivery_revenue_requirement: 501861831.36 +total_delivery_and_supply_revenue_requirement: 914812201.47 +total_residential_kwh: 3173819718.1 # sum(resstock_monthly_kwh) * resstock_scale_factor eia_total_residential_kwh: 3027334912.0 eia_year: 2024 -delivery_top_ups: +delivery_top_ups: # total_budgets are inflated by total_residential_kwh / eia_total_residential_kwh + o_m_charge: + charge_unit: $/kWh + budget_method: resstock + total_budget: 7113863.17 + o_m_reconciliation_factor: + charge_unit: $/kWh + budget_method: resstock + total_budget: 271250.31 + capex_factor: + charge_unit: $/kWh + budget_method: resstock + total_budget: 25291640.14 + capex_reconciliation_factor: + charge_unit: $/kWh + budget_method: resstock + total_budget: 963225.23 + revenue_decoupling_rdm: + charge_unit: $/kWh + budget_method: resstock + total_budget: -2361524.7 + pension_adjustment_factor: + charge_unit: $/kWh + budget_method: resstock + total_budget: -9727268.54 storm_fund_replenishment_factor: charge_unit: $/kWh budget_method: resstock @@ -17,6 +41,14 @@ delivery_top_ups: charge_unit: $/kWh budget_method: resstock total_budget: 238059.04 + low_income_discounts: + charge_unit: $/kWh + budget_method: resstock + total_budget: 8379079.61 + performance_incentive_factor: + charge_unit: $/kWh + budget_method: resstock + total_budget: 0.0 net_metering_charge: charge_unit: $/kWh budget_method: resstock @@ -33,15 +65,23 @@ delivery_top_ups: charge_unit: $/month customer_count: 451381 total_budget: 4279091.88 + transmission_charge: + charge_unit: $/kWh + budget_method: resstock + total_budget: 141769923.65 + transition_restructuring: + charge_unit: $/kWh + budget_method: resstock + total_budget: 15861.58 energy_efficiency_programs_charge: charge_unit: $/kWh budget_method: resstock total_budget: 34848540.5 - transmission_charge: +supply_top_ups: + last_resort_adjustment_factor: charge_unit: $/kWh budget_method: resstock - total_budget: 141769923.65 -supply_top_ups: + total_budget: 0.0 supply_commodity_bundled: charge_unit: $/kWh budget_method: resstock @@ -63,4 +103,4 @@ resstock_monthly_kwh: 10: 218918966.64 11: 253198388.79 12: 296743670.22 -resstock_scale_factor: 0.936677 +resstock_scale_factor: 0.936677 # eia_customer_count / resstock_customer_count_for_utility diff --git a/rate_design/hp_rates/ri/config/rev_requirement/top-ups/charge_decisions/rie_charge_decisions.json b/rate_design/hp_rates/ri/config/rev_requirement/top-ups/charge_decisions/rie_charge_decisions.json index ba80b8c9..1a319799 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/top-ups/charge_decisions/rie_charge_decisions.json +++ b/rate_design/hp_rates/ri/config/rev_requirement/top-ups/charge_decisions/rie_charge_decisions.json @@ -95,7 +95,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "already_in_drr", + "decision": "add_to_drr", "master_charge": "O&M Charge", "master_type": "Base delivery" }, @@ -120,7 +120,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "exclude_trueup", + "decision": "add_to_drr", "master_charge": "O&M Reconciliation Factor", "master_type": "Cost recon" }, @@ -145,7 +145,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "already_in_drr", + "decision": "add_to_drr", "master_charge": "CapEx Factor", "master_type": "Base delivery" }, @@ -170,7 +170,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "exclude_trueup", + "decision": "add_to_drr", "master_charge": "CapEx Reconciliation Factor", "master_type": "Cost recon" }, @@ -195,7 +195,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "exclude_trueup", + "decision": "add_to_drr", "master_charge": "Revenue Decoupling (RDM)", "master_type": "Revenue true-up" }, @@ -220,7 +220,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "already_in_drr", + "decision": "add_to_drr", "master_charge": "Pension Adjustment Factor", "master_type": "Base delivery" }, @@ -295,7 +295,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "exclude_eligibility", + "decision": "add_to_drr", "master_charge": "Low-income discounts", "master_type": "Eligibility" }, @@ -320,7 +320,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "exclude_negligible", + "decision": "add_to_drr", "master_charge": "Performance Incentive Factor", "master_type": "Performance incentive" }, @@ -345,7 +345,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "exclude_trueup", + "decision": "add_to_srr", "master_charge": "Last Resort Adjustment Factor", "master_type": "Cost recon" }, @@ -495,7 +495,7 @@ "season": null, "time_of_use": null, "source": "base_tariff", - "decision": "exclude_trueup", + "decision": "add_to_drr", "master_charge": "Transition / Restructuring", "master_type": "Cost recon" }, diff --git a/rate_design/hp_rates/ri/config/rev_requirement/top-ups/monthly_rates/rie_monthly_rates_2025.yaml b/rate_design/hp_rates/ri/config/rev_requirement/top-ups/monthly_rates/rie_monthly_rates_2025.yaml index 8fdb9f6d..4395f701 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/top-ups/monthly_rates/rie_monthly_rates_2025.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/top-ups/monthly_rates/rie_monthly_rates_2025.yaml @@ -5,6 +5,96 @@ end_month: 2025-12 add_to_drr: rate_structure: flat charges: + o_m_charge: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.00227 + 2025-02: 0.00227 + 2025-03: 0.00227 + 2025-04: 0.00223 + 2025-05: 0.00223 + 2025-06: 0.00223 + 2025-07: 0.00223 + 2025-08: 0.00223 + 2025-09: 0.00223 + 2025-10: 0.00223 + 2025-11: 0.00223 + 2025-12: 0.00223 + o_m_reconciliation_factor: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.0001 + 2025-02: 0.0001 + 2025-03: 0.0001 + 2025-04: 0.0001 + 2025-05: 0.0001 + 2025-06: 0.0001 + 2025-07: 0.0001 + 2025-08: 0.0001 + 2025-09: 0.0001 + 2025-10: 4.0e-05 + 2025-11: 4.0e-05 + 2025-12: 4.0e-05 + capex_factor: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.00709 + 2025-02: 0.00709 + 2025-03: 0.00709 + 2025-04: 0.00832 + 2025-05: 0.00832 + 2025-06: 0.00832 + 2025-07: 0.00832 + 2025-08: 0.00832 + 2025-09: 0.00832 + 2025-10: 0.00832 + 2025-11: 0.00832 + 2025-12: 0.00832 + capex_reconciliation_factor: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.0001 + 2025-02: 0.0001 + 2025-03: 0.0001 + 2025-04: 0.0001 + 2025-05: 0.0001 + 2025-06: 0.0001 + 2025-07: 0.0001 + 2025-08: 0.0001 + 2025-09: 0.0001 + 2025-10: 0.00094 + 2025-11: 0.00094 + 2025-12: 0.00094 + revenue_decoupling_rdm: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.00123 + 2025-02: 0.00123 + 2025-03: 0.00123 + 2025-04: 0.00123 + 2025-05: 0.00123 + 2025-06: 0.00123 + 2025-07: -0.00272 + 2025-08: -0.00272 + 2025-09: -0.00272 + 2025-10: -0.00272 + 2025-11: -0.00272 + 2025-12: -0.00272 + pension_adjustment_factor: + charge_unit: $/kWh + monthly_rates: + 2025-01: -0.00274 + 2025-02: -0.00274 + 2025-03: -0.00274 + 2025-04: -0.00274 + 2025-05: -0.00274 + 2025-06: -0.00274 + 2025-07: -0.00339 + 2025-08: -0.00339 + 2025-09: -0.00339 + 2025-10: -0.00339 + 2025-11: -0.00339 + 2025-12: -0.00339 storm_fund_replenishment_factor: charge_unit: $/kWh monthly_rates: @@ -35,7 +125,37 @@ add_to_drr: 2025-10: 6.0e-05 2025-11: 6.0e-05 2025-12: 6.0e-05 - net_metering_charge: # In 2095 RIE rate summary. + low_income_discounts: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.00277 + 2025-02: 0.00277 + 2025-03: 0.00277 + 2025-04: 0.00277 + 2025-05: 0.00277 + 2025-06: 0.00277 + 2025-07: 0.00251 + 2025-08: 0.00251 + 2025-09: 0.00251 + 2025-10: 0.00251 + 2025-11: 0.00251 + 2025-12: 0.00251 + performance_incentive_factor: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.0 + 2025-02: 0.0 + 2025-03: 0.0 + 2025-04: 0.0 + 2025-05: 0.0 + 2025-06: 0.0 + 2025-07: 0.0 + 2025-08: 0.0 + 2025-09: 0.0 + 2025-10: 0.0 + 2025-11: 0.0 + 2025-12: 0.0 + net_metering_charge: charge_unit: $/kWh monthly_rates: 2025-01: 0.01253 @@ -50,7 +170,7 @@ add_to_drr: 2025-10: 0.01457 2025-11: 0.01457 2025-12: 0.01457 - long_term_contracting_charge: # In 2095 RIE rate summary. + long_term_contracting_charge: charge_unit: $/kWh monthly_rates: 2025-01: 0.01092 @@ -65,7 +185,7 @@ add_to_drr: 2025-10: 0.00656 2025-11: 0.00656 2025-12: 0.00656 - re_growth_charge: # In 2095 RIE rate summary. + re_growth_charge: charge_unit: $/month monthly_rates: 2025-01: 4.02 @@ -79,8 +199,8 @@ add_to_drr: 2025-09: 5.75 2025-10: 3.22 2025-11: 3.22 - 2025-12: 3.22 # 3.22 month actual - liheap_enhancement_charge: # In 2095 RIE rate summary. + 2025-12: 3.22 + liheap_enhancement_charge: charge_unit: $/month monthly_rates: 2025-01: 0.79 @@ -94,23 +214,8 @@ add_to_drr: 2025-09: 0.79 2025-10: 0.79 2025-11: 0.79 - 2025-12: 0.79 # 0.78 actual - energy_efficiency_programs_charge: # In 2095 RIE rate summary. - charge_unit: $/kWh - monthly_rates: - 2025-01: 0.01098 - 2025-02: 0.01098 - 2025-03: 0.01098 - 2025-04: 0.01098 - 2025-05: 0.01098 - 2025-06: 0.01098 - 2025-07: 0.01098 - 2025-08: 0.01098 - 2025-09: 0.01098 - 2025-10: 0.01098 - 2025-11: 0.01098 - 2025-12: 0.01098 # .00195 actual - transmission_charge: # In 2095 RIE rate summary as Total Transmission Charge. + 2025-12: 0.79 + transmission_charge: charge_unit: $/kWh monthly_rates: 2025-01: 0.04161 @@ -124,10 +229,55 @@ add_to_drr: 2025-09: 0.04773 2025-10: 0.04773 2025-11: 0.04773 - 2025-12: 0.04773 # .04773 actual + 2025-12: 0.04773 + transition_restructuring: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.0 + 2025-02: 0.0 + 2025-03: 0.0 + 2025-04: 0.0 + 2025-05: 0.0 + 2025-06: 0.0 + 2025-07: 1.0e-05 + 2025-08: 1.0e-05 + 2025-09: 1.0e-05 + 2025-10: 1.0e-05 + 2025-11: 1.0e-05 + 2025-12: 1.0e-05 + energy_efficiency_programs_charge: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.01098 + 2025-02: 0.01098 + 2025-03: 0.01098 + 2025-04: 0.01098 + 2025-05: 0.01098 + 2025-06: 0.01098 + 2025-07: 0.01098 + 2025-08: 0.01098 + 2025-09: 0.01098 + 2025-10: 0.01098 + 2025-11: 0.01098 + 2025-12: 0.01098 add_to_srr: rate_structure: flat charges: + last_resort_adjustment_factor: + charge_unit: $/kWh + monthly_rates: + 2025-01: 0.0 + 2025-02: 0.0 + 2025-03: 0.0 + 2025-04: 0.0 + 2025-05: 0.0 + 2025-06: 0.0 + 2025-07: 0.0 + 2025-08: 0.0 + 2025-09: 0.0 + 2025-10: 0.0 + 2025-11: 0.0 + 2025-12: 0.0 supply_commodity_bundled: charge_unit: $/kWh monthly_rates: @@ -157,7 +307,7 @@ add_to_srr: 2025-09: 0.01461 2025-10: 0.01461 2025-11: 0.01461 - 2025-12: 0.01461 # .0198 actual + 2025-12: 0.01461 already_in_drr: rate_structure: flat charges: @@ -176,10 +326,10 @@ already_in_drr: 2025-10: 6.0 2025-11: 6.0 2025-12: 6.0 - core_delivery_rate: # In 2095 RIE rate summary has volumetric Billing Distribution Charge, none of this is broken out. + core_delivery_rate: charge_unit: $/kWh monthly_rates: - 2025-01: 0.0458 # .059 if next 3 charges included, .0534 actual, .0645 proposed + 2025-01: 0.0458 2025-02: 0.0458 2025-03: 0.0458 2025-04: 0.0458 @@ -191,67 +341,8 @@ already_in_drr: 2025-10: 0.0458 2025-11: 0.0458 2025-12: 0.0458 - o_m_charge: - charge_unit: $/kWh - monthly_rates: - 2025-01: 0.00227 - 2025-02: 0.00227 - 2025-03: 0.00227 - 2025-04: 0.00223 - 2025-05: 0.00223 - 2025-06: 0.00223 - 2025-07: 0.00223 - 2025-08: 0.00223 - 2025-09: 0.00223 - 2025-10: 0.00223 - 2025-11: 0.00223 - 2025-12: 0.00223 - capex_factor: - charge_unit: $/kWh - monthly_rates: - 2025-01: 0.00709 - 2025-02: 0.00709 - 2025-03: 0.00709 - 2025-04: 0.00832 - 2025-05: 0.00832 - 2025-06: 0.00832 - 2025-07: 0.00832 - 2025-08: 0.00832 - 2025-09: 0.00832 - 2025-10: 0.00832 - 2025-11: 0.00832 - 2025-12: 0.00832 - pension_adjustment_factor: - charge_unit: $/kWh - monthly_rates: - 2025-01: 0.00274 - 2025-02: 0.00274 - 2025-03: 0.00274 - 2025-04: 0.00274 - 2025-05: 0.00274 - 2025-06: 0.00274 - 2025-07: 0.00339 - 2025-08: 0.00339 - 2025-09: 0.00339 - 2025-10: 0.00339 - 2025-11: 0.00339 - 2025-12: 0.00339 excluded: gross_earnings_tax: decision: exclude_percentage - o_m_reconciliation_factor: - decision: exclude_trueup - capex_reconciliation_factor: - decision: exclude_trueup - rdm_adjustment_factor: - decision: exclude_trueup - low_income_discount_recovery_factor: - decision: exclude_eligibility - performance_incentive_factor: - decision: exclude_negligible - last_resort_adjustment_factor: - decision: exclude_trueup - transition_charge: # In 2095 RIE rate summary. - decision: exclude_trueup minimum: decision: exclude_redundant From f79cc71aa52d64f6285ddcc996208dc97a1a100e Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 05:25:32 +0000 Subject: [PATCH 38/40] Regenerate rie default, flat, nonhp_default and nonhp_flat based on new revenue requirement --- .../config/tariffs/electric/rie_default.json | 301 +++++++++--------- .../tariffs/electric/rie_default_supply.json | 8 +- .../ri/config/tariffs/electric/rie_flat.json | 2 +- .../tariffs/electric/rie_flat_supply.json | 2 +- .../tariffs/electric/rie_nonhp_default.json | 301 +++++++++--------- .../electric/rie_nonhp_default_supply.json | 8 +- .../tariffs/electric/rie_nonhp_flat.json | 2 +- .../electric/rie_nonhp_flat_supply.json | 2 +- 8 files changed, 320 insertions(+), 306 deletions(-) diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_default.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_default.json index bcf0b86d..33ebb0fb 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_default.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_default.json @@ -240,82 +240,82 @@ 2 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ] ], "energyweekendschedule": [ @@ -554,102 +554,109 @@ 2 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ] ], "energyratestructure": [ [ { - "rate": 0.14191, + "rate": 0.14063, + "adj": 0.0, + "unit": "kWh" + } + ], + [ + { + "rate": 0.1395, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.14078, + "rate": 0.14074, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.14752, + "rate": 0.14152, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_supply.json index c55f029c..a2bd5f8a 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_supply.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_supply.json @@ -635,28 +635,28 @@ "energyratestructure": [ [ { - "rate": 0.30578, + "rate": 0.3045, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.24146, + "rate": 0.24018, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.2482, + "rate": 0.24142, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.29522, + "rate": 0.28922, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_flat.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_flat.json index 8735cebc..4445834a 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_flat.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_flat.json @@ -635,7 +635,7 @@ "energyratestructure": [ [ { - "rate": 0.1513638, + "rate": 0.13854179, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_flat_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_flat_supply.json index 83dc9921..6e88d3cb 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_flat_supply.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_flat_supply.json @@ -635,7 +635,7 @@ "energyratestructure": [ [ { - "rate": 0.28147528, + "rate": 0.26865327, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default.json index 202eaa73..c37917aa 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default.json @@ -240,82 +240,82 @@ 2 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ] ], "energyweekendschedule": [ @@ -554,102 +554,109 @@ 2 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ] ], "energyratestructure": [ [ { - "rate": 0.14191, + "rate": 0.14063, + "adj": 0.0, + "unit": "kWh" + } + ], + [ + { + "rate": 0.1395, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.14078, + "rate": 0.14074, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.14752, + "rate": 0.14152, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply.json index cc330c13..58997051 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply.json @@ -635,28 +635,28 @@ "energyratestructure": [ [ { - "rate": 0.30578, + "rate": 0.3045, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.24146, + "rate": 0.24018, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.2482, + "rate": 0.24142, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.29522, + "rate": 0.28922, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_flat.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_flat.json index 583451e9..58bcd7cd 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_flat.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_flat.json @@ -635,7 +635,7 @@ "energyratestructure": [ [ { - "rate": 0.1513638, + "rate": 0.13854179, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_flat_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_flat_supply.json index a16b5f11..3964b856 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_flat_supply.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_flat_supply.json @@ -635,7 +635,7 @@ "energyratestructure": [ [ { - "rate": 0.28147528, + "rate": 0.26865327, "adj": 0.0, "unit": "kWh" } From 8ea3463e8fb2470c0ef682aaca85fdb010f3d3c6 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 05:25:57 +0000 Subject: [PATCH 39/40] Remove annoying lack of newline in create-seasonal-tou-tariff --- rate_design/hp_rates/Justfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rate_design/hp_rates/Justfile b/rate_design/hp_rates/Justfile index 6676dbd2..48d7b500 100644 --- a/rate_design/hp_rates/Justfile +++ b/rate_design/hp_rates/Justfile @@ -265,7 +265,7 @@ create-seasonal-tou-tariffs base_pattern="flat": t = json.load(open('${base_path}')); \ t['items'][0]['label'] = '${supply_label}'; \ t['items'][0].get('name') and t['items'][0].update({'name': '${supply_label}'}); \ - json.dump(t, open('${supply_path}', 'w'), indent=2); \ + f = open('${supply_path}', 'w'); json.dump(t, f, indent=2); f.write('\n'); f.close(); \ print('Created ${supply_path}')" echo ">> create-seasonal-tou-tariffs: wrote {{ tou_tariff_key }}.json and {{ tou_tariff_key }}_supply.json" >&2 From 983267ecbf08007c0b14dd0786929a28186e0d78 Mon Sep 17 00:00:00 2001 From: Juan-Pablo Velez Date: Mon, 30 Mar 2026 05:43:50 +0000 Subject: [PATCH 40/40] New subclass_rr and calibrated rates --- .../rev_requirement/rie_hp_vs_nonhp.yaml | 116 +++---- .../electric/rie_default_calibrated.json | 301 +++++++++--------- .../rie_default_supply_calibrated.json | 8 +- .../config/tariffs/electric/rie_hp_flat.json | 2 +- .../electric/rie_hp_flat_calibrated.json | 2 +- .../tariffs/electric/rie_hp_flat_supply.json | 2 +- .../rie_hp_flat_supply_calibrated.json | 2 +- .../tariffs/electric/rie_hp_seasonal.json | 4 +- .../rie_hp_seasonalTOU_calibrated.json | 8 +- .../rie_hp_seasonalTOU_flex_calibrated.json | 8 +- ...hp_seasonalTOU_flex_supply_calibrated.json | 8 +- .../rie_hp_seasonalTOU_supply_calibrated.json | 8 +- .../electric/rie_hp_seasonal_calibrated.json | 4 +- .../electric/rie_hp_seasonal_supply.json | 4 +- .../rie_hp_seasonal_supply_calibrated.json | 4 +- .../rie_nonhp_default_calibrated.json | 301 +++++++++--------- .../rie_nonhp_default_supply_calibrated.json | 8 +- 17 files changed, 402 insertions(+), 388 deletions(-) diff --git a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml index 5ee9c67a..f808aeb5 100644 --- a/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml +++ b/rate_design/hp_rates/ri/config/rev_requirement/rie_hp_vs_nonhp.yaml @@ -1,93 +1,93 @@ utility: rie group_col: has_hp -source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260329_supply_passthrough/20260329_185643_ri_rie_run1_up00_precalc__default -total_delivery_revenue_requirement: 542556587.55 -total_delivery_and_supply_revenue_requirement: 955506957.66 +source_run_dir: s3://data.sb/switchbox/cairo/outputs/hp_rates/ri/rie/ri_20260330_170M-rr-and-all-topups/20260330_052822_ri_rie_run1_up00_precalc__default +total_delivery_revenue_requirement: 501861831.36 +total_delivery_and_supply_revenue_requirement: 914812201.47 subclass_revenue_requirements: delivery: percustomer: - non-hp: 531018474.41656953 - hp: 11538113.133430582 + non-hp: 491154658.2744282 + hp: 10707173.085571948 epmc: - non-hp: 528045651.5245808 - hp: 14510936.025419153 + non-hp: 488439296.0971811 + hp: 13422535.262819069 volumetric: - non-hp: 518391179.89397407 - hp: 24165407.656025995 + non-hp: 479620947.99521303 + hp: 22240883.364787325 passthrough: - non-hp: 518739812.78349507 - hp: 23816774.766508 + non-hp: 479891566.946397 + hp: 21970264.413606133 supply: passthrough: - non-hp: 391820543.3235589 - hp: 21129826.786441203 + non-hp: 391876320.0391297 + hp: 21074050.07087053 percustomer: non-hp: 394774670.83571476 hp: 18175699.27428508 volumetric: - non-hp: 391562609.286075 - hp: 21387760.823924605 + non-hp: 391562609.28607535 + hp: 21387760.8239246 heating_type_breakdown: percustomer: delivered_fuels: - delivery: 181332489.1256645 - supply: 135908283.90410486 - total: 317240773.02976936 + delivery: 168037448.35992628 + supply: 135908283.90410483 + total: 303945732.2640311 electrical_resistance: - delivery: 44312079.810387164 - supply: 66102143.52061756 - total: 110414223.33100472 + delivery: 40881788.84358613 + supply: 66102143.52061753 + total: 106983932.36420366 heat_pump: - delivery: 11538113.133430585 - supply: 18175699.274285078 - total: 29713812.407715663 + delivery: 10707173.085571941 + supply: 18175699.27428508 + total: 28882872.359857023 natgas: - delivery: 294621861.5284703 - supply: 184890391.1954931 - total: 479512252.7239634 + delivery: 272271704.8565803 + supply: 184890391.19549304 + total: 457162096.05207336 other: - delivery: 10752043.952047499 + delivery: 9963716.214335458 supply: 7873852.215499504 - total: 18625896.167547002 + total: 17837568.429834962 epmc: delivered_fuels: - delivery: 207705127.43377417 - supply: 117706857.03651214 - total: 325411984.4702863 + delivery: 192126089.75496 + supply: 119426695.40425807 + total: 311552785.1592181 electrical_resistance: - delivery: 35118464.54736015 - supply: 123390160.52429956 - total: 158508625.0716597 + delivery: 32484384.738329604 + supply: 119273405.29273526 + total: 151757790.03106487 heat_pump: - delivery: 14510936.025419157 - supply: 31630893.341966923 - total: 46141829.36738608 + delivery: 13422535.262819065 + supply: 30754127.36118207 + total: 44176662.62400114 natgas: - delivery: 272906529.7775144 - supply: 133725206.15449703 - total: 406631735.9320114 + delivery: 252437024.942811 + supply: 136876386.70049548 + total: 389313411.6433065 other: - delivery: 12315529.765931955 - supply: 6497253.052724594 - total: 18812782.81865655 + delivery: 11391796.661080286 + supply: 6619755.351329148 + total: 18011552.012409434 volumetric: delivered_fuels: - delivery: 188109558.81788254 - supply: 137632197.52143997 - total: 325741756.3393225 + delivery: 174227591.32619876 + supply: 137632197.5214398 + total: 311859788.84763855 electrical_resistance: - delivery: 84781682.33077732 - supply: 76396577.84717363 - total: 161178260.17795095 + delivery: 77846529.76650916 + supply: 76396577.84717359 + total: 154243107.61368275 heat_pump: - delivery: 24165407.656026 - supply: 21387760.82392461 - total: 45553168.47995061 + delivery: 22240883.364787325 + supply: 21387760.8239246 + total: 43628644.18871193 natgas: - delivery: 234571249.00894436 + delivery: 217421763.27564397 supply: 169615047.52142903 - total: 404186296.5303734 + total: 387036810.797073 other: - delivery: 10928689.736369764 - supply: 7918786.396032965 - total: 18847476.13240273 + delivery: 10125063.626860928 + supply: 7918786.396032959 + total: 18043850.022893887 diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_calibrated.json index 13cd9645..a4538ece 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_calibrated.json @@ -245,82 +245,82 @@ 2 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ] ], "energyweekendschedule": [ @@ -559,102 +559,109 @@ 2 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ] ], "energyratestructure": [ [ { - "rate": 0.14905972359481287, + "rate": 0.13890617405613098, + "adj": 0.0, + "unit": "kWh" + } + ], + [ + { + "rate": 0.13779007445306118, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.14787670991548896, + "rate": 0.1390164061156934, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.15496000432044088, + "rate": 0.13978803053263056, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_supply_calibrated.json index 778f4d82..f0850ad1 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_default_supply_calibrated.json @@ -640,28 +640,28 @@ "energyratestructure": [ [ { - "rate": 0.3142749014366787, + "rate": 0.30293911430095805, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.24816400934671407, + "rate": 0.23894866248695223, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.25508778520748737, + "rate": 0.24019119553188442, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.3034053178272926, + "rate": 0.28774197936678786, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat.json index 01b3bab1..6d17b0c3 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.08892178419071482, + "rate": 0.08161261279916143, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json index 5a3de9ef..15f66362 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_calibrated.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.08892178419071475, + "rate": 0.08161261279916142, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json index 0b4dc1dc..e99f6c38 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.3013395154610198, + "rate": 0.2881424091799955, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json index b17ca6b1..b1560a14 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_flat_supply_calibrated.json @@ -640,7 +640,7 @@ "energyratestructure": [ [ { - "rate": 0.23081944795841336, + "rate": 0.22313570720989814, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal.json index 1693c4ba..458fb347 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal.json @@ -640,14 +640,14 @@ "energyratestructure": [ [ { - "rate": 0.15141528165784907, + "rate": 0.13901501411908357, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.03576594732456555, + "rate": 0.03293105696261725, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_calibrated.json index fe03147f..c8db9f77 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_calibrated.json @@ -640,28 +640,28 @@ "energyratestructure": [ [ { - "rate": 0.05907650097888958, + "rate": 0.05429591985152336, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.07606834641952862, + "rate": 0.06991275332824007, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.05765811143752643, + "rate": 0.05299230904891993, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.19754822140725306, + "rate": 0.18156224926340947, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_flex_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_flex_calibrated.json index 49ae1daa..cd3df722 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_flex_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_flex_calibrated.json @@ -640,28 +640,28 @@ "energyratestructure": [ [ { - "rate": 0.058434311335117076, + "rate": 0.05366757247228269, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.07356295453977887, + "rate": 0.06756210698535667, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.05887841210126397, + "rate": 0.05407544602307204, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.20729571946132783, + "rate": 0.19038571334542284, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_flex_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_flex_supply_calibrated.json index 0baeed23..e6898efb 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_flex_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_flex_supply_calibrated.json @@ -640,28 +640,28 @@ "energyratestructure": [ [ { - "rate": 0.16454822470198638, + "rate": 0.15971893227440717, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.211876315488318, + "rate": 0.20565800053642924, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.16059752557289322, + "rate": 0.15588418141167987, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.5502392421178468, + "rate": 0.5340903823526976, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_supply_calibrated.json index ae6cc2fc..140f6941 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonalTOU_supply_calibrated.json @@ -640,28 +640,28 @@ "energyratestructure": [ [ { - "rate": 0.1636453024841859, + "rate": 0.1588647213568197, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.21071369077433774, + "rate": 0.20455809768304922, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.15971628194825874, + "rate": 0.15505047955965226, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.5472199252111241, + "rate": 0.5312339530672806, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_calibrated.json index 471e0ac2..f71b936e 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_calibrated.json @@ -640,14 +640,14 @@ "energyratestructure": [ [ { - "rate": 0.15236884092316022, + "rate": 0.13988887280258389, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.03599122261087993, + "rate": 0.033138028332445134, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_supply.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_supply.json index 505361bf..285bfc51 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_supply.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_supply.json @@ -640,14 +640,14 @@ "energyratestructure": [ [ { - "rate": 0.29331294542554764, + "rate": 0.2805381085298204, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.14983949715169062, + "rate": 0.1471553829647129, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_supply_calibrated.json index dec3dbb5..682b84c7 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_hp_seasonal_supply_calibrated.json @@ -640,14 +640,14 @@ "energyratestructure": [ [ { - "rate": 0.29413619422144566, + "rate": 0.28129254285709443, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.15026114647328004, + "rate": 0.14755169054610492, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_calibrated.json index c0824e0e..a7e37c98 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_calibrated.json @@ -245,82 +245,82 @@ 2 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ] ], "energyweekendschedule": [ @@ -559,102 +559,109 @@ 2 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ], [ - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2, - 2 + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3, + 3 ] ], "energyratestructure": [ [ { - "rate": 0.15208952518953966, + "rate": 0.1417400447851933, + "adj": 0.0, + "unit": "kWh" + } + ], + [ + { + "rate": 0.14060117526554242, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.15088246546581316, + "rate": 0.14185252572540574, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.1581097355616256, + "rate": 0.14263989230689278, "adj": 0.0, "unit": "kWh" } diff --git a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply_calibrated.json b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply_calibrated.json index 9ca11210..6d214e09 100644 --- a/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply_calibrated.json +++ b/rate_design/hp_rates/ri/config/tariffs/electric/rie_nonhp_default_supply_calibrated.json @@ -640,28 +640,28 @@ "energyratestructure": [ [ { - "rate": 0.313832800481621, + "rate": 0.3061325834705327, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.24781490878207596, + "rate": 0.2414675685995683, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.2547289447370959, + "rate": 0.24272319995628608, "adj": 0.0, "unit": "kWh" } ], [ { - "rate": 0.30297850747696603, + "rate": 0.29077524610760014, "adj": 0.0, "unit": "kWh" }