Skip to content

Commit 3535dd6

Browse files
committed
Add mixed adoption trajectory utilities and scenario script
1 parent 45a3012 commit 3535dd6

File tree

3 files changed

+661
-0
lines changed

3 files changed

+661
-0
lines changed
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
#!/usr/bin/env python
2+
"""Generate NY heat pump adoption scenarios with cumulative adoption."""
3+
4+
from pathlib import Path
5+
6+
from utils.mixed_adoption_trajectory import (
7+
build_adoption_trajectory,
8+
fetch_baseline_sample,
9+
)
10+
11+
# Base data directory for NY HP rates (git-ignored raw/processed, configs versioned)
12+
BASE_DATA_DIR = Path("rate_design/ny/hp_rates/data")
13+
14+
# Configuration
15+
CONFIG = {
16+
# ResStock release parameters
17+
"release_year": "2024",
18+
"weather_file": "tmy3",
19+
"release_version": "2",
20+
"state": "NY",
21+
# Heat pump upgrade ID (adjust based on your ResStock release)
22+
"hp_upgrade_id": "1",
23+
# Download settings
24+
"output_dir": BASE_DATA_DIR / "buildstock_raw",
25+
"max_workers": 5,
26+
# Sampling settings
27+
"sample_size": 1000, # Number of buildings to sample
28+
"sample_seed": 123, # Seed for sampling reproducibility (determines building ordering)
29+
# Adoption scenario settings
30+
"adoption_fractions": [0.1, 0.2, 0.3, 0.5, 0.8, 1.0],
31+
# Output settings
32+
"processed_dir": BASE_DATA_DIR / "buildstock_processed",
33+
}
34+
35+
36+
def main():
37+
"""Run the complete workflow to generate adoption scenarios."""
38+
print("=" * 80)
39+
print("NY Heat Pump Cumulative Adoption Scenario Generator")
40+
print("=" * 80)
41+
print("\nConfiguration:")
42+
for key, value in CONFIG.items():
43+
print(f" {key}: {value}")
44+
print("\n")
45+
46+
# Step 1: Fetch baseline sample and establish building ID ordering
47+
print("\n" + "=" * 80)
48+
print("STEP 1: Fetching baseline sample")
49+
print("=" * 80)
50+
print(f"Fetching {CONFIG['sample_size']} baseline buildings (seed={CONFIG['sample_seed']})")
51+
52+
baseline_metadata_path, building_ids = fetch_baseline_sample(
53+
sample_size=CONFIG["sample_size"],
54+
random_seed=CONFIG["sample_seed"],
55+
release_year=CONFIG["release_year"],
56+
weather_file=CONFIG["weather_file"],
57+
release_version=CONFIG["release_version"],
58+
state=CONFIG["state"],
59+
output_dir=CONFIG["output_dir"],
60+
max_workers=CONFIG["max_workers"],
61+
)
62+
63+
print(f"\n✓ Fetched {len(building_ids)} baseline buildings")
64+
print(f"✓ Baseline metadata: {baseline_metadata_path}")
65+
print(f"✓ Building ID ordering established (deterministic from seed)")
66+
67+
# Step 2: Build adoption trajectory
68+
print("\n" + "=" * 80)
69+
print("STEP 2: Building adoption trajectory")
70+
print("=" * 80)
71+
print(f"Creating scenarios for adoption fractions: {CONFIG['adoption_fractions']}")
72+
print("Note: Upgrade data will be fetched incrementally for each fraction")
73+
74+
scenario_paths = build_adoption_trajectory(
75+
baseline_metadata_path=baseline_metadata_path,
76+
baseline_building_ids=building_ids,
77+
adoption_fractions=CONFIG["adoption_fractions"],
78+
upgrade_id=CONFIG["hp_upgrade_id"],
79+
release_year=CONFIG["release_year"],
80+
weather_file=CONFIG["weather_file"],
81+
release_version=CONFIG["release_version"],
82+
state=CONFIG["state"],
83+
output_dir=CONFIG["output_dir"],
84+
max_workers=CONFIG["max_workers"],
85+
output_processed_dir=CONFIG["processed_dir"],
86+
)
87+
88+
# Summary
89+
print("\n" + "=" * 80)
90+
print("COMPLETE - Scenario Summary")
91+
print("=" * 80)
92+
print(f"\nGenerated {len(scenario_paths)} adoption scenarios:")
93+
for fraction, path in sorted(scenario_paths.items()):
94+
n_adopters = int(round(fraction * len(building_ids)))
95+
print(f" {fraction*100:3.0f}% adoption ({n_adopters:4d} buildings) → {path.name}")
96+
97+
print(f"\nAll scenarios use seed {CONFIG['sample_seed']} ensuring:")
98+
print(" - Reproducibility: Re-running with same seed gives identical results")
99+
print(" - Cumulative property: Adopters at X% ⊆ Adopters at Y% for X < Y")
100+
print(" - Efficiency: Upgrade data fetched only for buildings that adopt")
101+
102+
print("\nNext steps:")
103+
print(" - Load scenarios with: pl.read_parquet(path)")
104+
print(" - Check 'adopted' column (0=baseline, 1=upgrade)")
105+
print(" - Use for GenX/CAIRO modeling")
106+
print("\n✓ Done!")
107+
108+
109+
if __name__ == "__main__":
110+
main()

tests/test_cumulative_adoption.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
"""Tests for cumulative adoption sampling utilities."""
2+
3+
import numpy as np
4+
import pytest
5+
6+
from rate_design.utils.resstock_cumulative_adoption import (
7+
generate_random_ordering,
8+
select_cumulative_adopters,
9+
)
10+
11+
12+
class TestRandomOrdering:
13+
"""Tests for generate_random_ordering function."""
14+
15+
def test_reproducibility_with_seed(self):
16+
"""Test that same seed produces same ordering."""
17+
bldg_ids = list(range(100))
18+
seed = 42
19+
20+
ordering1 = generate_random_ordering(bldg_ids, seed=seed)
21+
ordering2 = generate_random_ordering(bldg_ids, seed=seed)
22+
23+
assert ordering1 == ordering2
24+
25+
def test_different_seeds_produce_different_orderings(self):
26+
"""Test that different seeds produce different orderings."""
27+
bldg_ids = list(range(100))
28+
29+
ordering1 = generate_random_ordering(bldg_ids, seed=42)
30+
ordering2 = generate_random_ordering(bldg_ids, seed=43)
31+
32+
assert ordering1 != ordering2
33+
34+
def test_all_ids_present(self):
35+
"""Test that shuffling preserves all IDs."""
36+
bldg_ids = list(range(50))
37+
ordering = generate_random_ordering(bldg_ids, seed=42)
38+
39+
assert set(ordering) == set(bldg_ids)
40+
assert len(ordering) == len(bldg_ids)
41+
42+
def test_ordering_is_permutation(self):
43+
"""Test that output is a valid permutation."""
44+
bldg_ids = [10, 20, 30, 40, 50]
45+
ordering = generate_random_ordering(bldg_ids, seed=42)
46+
47+
assert sorted(ordering) == sorted(bldg_ids)
48+
49+
def test_none_seed_is_non_deterministic(self):
50+
"""Test that None seed produces different results (with high probability)."""
51+
bldg_ids = list(range(100))
52+
53+
# Run multiple times - should get different results
54+
orderings = [generate_random_ordering(bldg_ids, seed=None) for _ in range(5)]
55+
56+
# At least some should be different (extremely unlikely to get 5 identical random shuffles)
57+
unique_orderings = [tuple(o) for o in orderings]
58+
assert len(set(unique_orderings)) > 1
59+
60+
61+
class TestCumulativeAdopters:
62+
"""Tests for select_cumulative_adopters function."""
63+
64+
def test_zero_fraction(self):
65+
"""Test that 0% adoption returns empty set."""
66+
ordering = list(range(100))
67+
adopters = select_cumulative_adopters(ordering, 0.0)
68+
69+
assert len(adopters) == 0
70+
assert isinstance(adopters, set)
71+
72+
def test_full_adoption(self):
73+
"""Test that 100% adoption returns all buildings."""
74+
ordering = list(range(100))
75+
adopters = select_cumulative_adopters(ordering, 1.0)
76+
77+
assert len(adopters) == 100
78+
assert adopters == set(ordering)
79+
80+
def test_fraction_rounding(self):
81+
"""Test proper rounding of fractional counts."""
82+
ordering = list(range(100))
83+
84+
# 10% of 100 = 10
85+
adopters = select_cumulative_adopters(ordering, 0.1)
86+
assert len(adopters) == 10
87+
88+
# 25% of 100 = 25
89+
adopters = select_cumulative_adopters(ordering, 0.25)
90+
assert len(adopters) == 25
91+
92+
def test_cumulative_property(self):
93+
"""Test that adopters at lower fractions are subset of higher fractions."""
94+
ordering = list(range(1000))
95+
96+
adopters_10 = select_cumulative_adopters(ordering, 0.1)
97+
adopters_20 = select_cumulative_adopters(ordering, 0.2)
98+
adopters_50 = select_cumulative_adopters(ordering, 0.5)
99+
adopters_80 = select_cumulative_adopters(ordering, 0.8)
100+
101+
# Check cumulative property
102+
assert adopters_10.issubset(adopters_20)
103+
assert adopters_20.issubset(adopters_50)
104+
assert adopters_50.issubset(adopters_80)
105+
106+
# Verify sizes
107+
assert len(adopters_10) == 100
108+
assert len(adopters_20) == 200
109+
assert len(adopters_50) == 500
110+
assert len(adopters_80) == 800
111+
112+
def test_selects_from_beginning_of_ordering(self):
113+
"""Test that selection comes from beginning of ordering."""
114+
ordering = [10, 20, 30, 40, 50, 60, 70, 80, 90, 100]
115+
116+
adopters = select_cumulative_adopters(ordering, 0.3) # 30% = 3 buildings
117+
118+
# Should be first 3 from ordering
119+
assert adopters == {10, 20, 30}
120+
121+
def test_invalid_fraction_raises_error(self):
122+
"""Test that invalid fractions raise ValueError."""
123+
ordering = list(range(100))
124+
125+
with pytest.raises(ValueError):
126+
select_cumulative_adopters(ordering, -0.1)
127+
128+
with pytest.raises(ValueError):
129+
select_cumulative_adopters(ordering, 1.5)
130+
131+
def test_edge_case_single_building(self):
132+
"""Test with single building."""
133+
ordering = [42]
134+
135+
adopters_0 = select_cumulative_adopters(ordering, 0.0)
136+
adopters_50 = select_cumulative_adopters(ordering, 0.5)
137+
adopters_100 = select_cumulative_adopters(ordering, 1.0)
138+
139+
assert len(adopters_0) == 0
140+
assert len(adopters_50) == 0 # 0.5 * 1 = 0.5, rounds to 0
141+
assert len(adopters_100) == 1
142+
143+
144+
class TestIntegration:
145+
"""Integration tests combining ordering and selection."""
146+
147+
def test_end_to_end_cumulative_workflow(self):
148+
"""Test complete workflow for generating cumulative adoption sets."""
149+
# Simulate a cohort
150+
bldg_ids = list(range(1000, 2000)) # 1000 buildings
151+
seed = 12345
152+
153+
# Generate ordering
154+
ordering = generate_random_ordering(bldg_ids, seed=seed)
155+
156+
# Generate multiple adoption fractions
157+
fractions = [0.0, 0.1, 0.2, 0.3, 0.5, 0.8, 1.0]
158+
adoption_sets = {}
159+
160+
for f in fractions:
161+
adoption_sets[f] = select_cumulative_adopters(ordering, f)
162+
163+
# Verify cumulative property across all fractions
164+
for i in range(len(fractions) - 1):
165+
f1 = fractions[i]
166+
f2 = fractions[i + 1]
167+
assert adoption_sets[f1].issubset(adoption_sets[f2])
168+
169+
# Verify sizes
170+
assert len(adoption_sets[0.0]) == 0
171+
assert len(adoption_sets[0.1]) == 100
172+
assert len(adoption_sets[0.5]) == 500
173+
assert len(adoption_sets[1.0]) == 1000
174+
175+
def test_reproducibility_across_sessions(self):
176+
"""Test that workflow is reproducible across different runs."""
177+
bldg_ids = list(range(500))
178+
seed = 999
179+
180+
# Session 1
181+
ordering1 = generate_random_ordering(bldg_ids, seed=seed)
182+
adopters1_20 = select_cumulative_adopters(ordering1, 0.2)
183+
adopters1_50 = select_cumulative_adopters(ordering1, 0.5)
184+
185+
# Session 2 (simulating re-run)
186+
ordering2 = generate_random_ordering(bldg_ids, seed=seed)
187+
adopters2_20 = select_cumulative_adopters(ordering2, 0.2)
188+
adopters2_50 = select_cumulative_adopters(ordering2, 0.5)
189+
190+
# Should be identical
191+
assert ordering1 == ordering2
192+
assert adopters1_20 == adopters2_20
193+
assert adopters1_50 == adopters2_50

0 commit comments

Comments
 (0)