Coverage for tests / test_timeslices.py: 99%
79 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-01 22:14 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-01 22:14 +0000
1"""Tests for solar-script timeslice helpers."""
3import importlib.util
4from pathlib import Path
6import pandas as pd
9def ensure_stage_0_config():
10 """
11 Create the minimal normalized SysSettings file expected by stage_0 imports.
12 """
13 repo_root = Path(__file__).resolve().parents[1]
14 stage_0_config = repo_root / "data_intermediate/stage_0_config"
15 stage_0_config.mkdir(parents=True, exist_ok=True)
16 syssettings_toml = stage_0_config / "SysSettings.toml"
17 syssettings_toml.write_text(
18 """
19[StartYear.Data]
20StartYear = [2023]
22[ActivePDef.Data]
23ActivePDef = ["5Year_increments"]
24""".strip() + "\n",
25 encoding="utf-8",
26 )
29def load_solar_run_hourly_profiles():
30 """
31 Load the script module directly from its path for test usage.
32 """
33 ensure_stage_0_config()
34 module_path = (
35 Path(__file__).resolve().parents[1]
36 / "scripts/stage_3_scenarios/electricity/solar_run_hourly_profiles.py"
37 )
38 spec = importlib.util.spec_from_file_location(
39 "solar_run_hourly_profiles", module_path
40 )
41 module = importlib.util.module_from_spec(spec)
42 assert spec.loader is not None
43 spec.loader.exec_module(module)
44 return module
47def write_test_epw(
48 path: Path,
49 header_year: int,
50 start_weekday: str = "Sunday",
51 days_in_year: int = 365,
52):
53 """
54 Write a minimal synthetic EPW file for calendar and timeslice tests.
55 """
56 header = [
57 [
58 "LOCATION",
59 "Test",
60 "Test",
61 "New Zealand",
62 "TMY2",
63 "0",
64 "0",
65 "0",
66 "0",
67 "0",
68 ],
69 ["DESIGN CONDITIONS", "0"],
70 ["TYPICAL/EXTREME PERIODS", "0"],
71 ["GROUND TEMPERATURES", "0"],
72 ["HOLIDAYS/DAYLIGHT SAVING", "No", "0", "0", "0"],
73 ["COMMENTS 1", "Synthetic test EPW"],
74 ["COMMENTS 2", "Synthetic test EPW"],
75 ["DATA PERIODS", "1", "1", "TMY2 Year", start_weekday, "1", str(days_in_year)],
76 ]
77 rows = []
78 for day in pd.date_range(f"{header_year}-01-01", f"{header_year}-12-31", freq="D"):
79 for hour in range(1, 25):
80 rows.append(
81 [
82 f"{day.year:04d}",
83 f"{day.month:02d}",
84 f"{day.day:02d}",
85 f"{hour:02d}",
86 "60",
87 ]
88 )
90 with path.open("w", encoding="utf-8", newline="") as handle:
91 for row in header + rows:
92 handle.write(",".join(row) + "\n")
95def test_create_timeslices_uses_project_time_of_day_and_daytype_definitions():
96 """
97 Shared timeslice construction should match the active repo settings.
98 """
99 module = load_solar_run_hourly_profiles()
100 df = pd.DataFrame(
101 {
102 "Trading_Date": pd.to_datetime(
103 ["2023-01-02", "2023-01-07", "2023-07-03", "2023-09-10"]
104 ),
105 "Hour": [18, 18, 7, 19],
106 }
107 )
109 result = module.create_timeslices(df)
111 assert result["TimeSlice"].tolist() == [
112 "SUM-WK-P",
113 "SUM-WE-P",
114 "WIN-WK-D",
115 "SPR-WE-N",
116 ]
119def test_create_timeslices_can_use_wall_clock_hour_column():
120 """
121 Timeslice construction should support solar wall-clock hours.
122 """
123 module = load_solar_run_hourly_profiles()
124 df = pd.DataFrame(
125 {
126 "WallClock_Date": pd.to_datetime(["2023-01-02", "2023-07-03"]),
127 "WallClock_Hour": [19, 18],
128 }
129 )
131 result = module.create_timeslices(
132 df,
133 date_col="WallClock_Date",
134 hour_col="WallClock_Hour",
135 )
137 assert result["TimeSlice"].tolist() == ["SUM-WK-N", "WIN-WK-P"]
140def test_build_time_index_uses_model_base_year_and_ignores_epw_calendar_metadata(
141 tmp_path,
142):
143 """
144 Solar timeslices should use the model base year rather than the EPW calendar.
145 """
146 module = load_solar_run_hourly_profiles()
148 epw_files = {}
149 for zone in module.ZONE_ORDER:
150 path = tmp_path / f"TMY_NZ_{zone}.epw"
151 write_test_epw(
152 path,
153 1999 if zone == "AK" else 2007,
154 start_weekday="Monday",
155 )
156 epw_files[zone] = path
158 time_index = module.build_time_index(epw_files)
160 assert time_index["Trading_Date"].min() == pd.Timestamp("2023-01-01")
161 assert time_index["Trading_Date"].max() == pd.Timestamp("2023-12-31")
162 assert time_index.iloc[0]["Trading_Date"].day_name() == "Sunday"
163 assert time_index.iloc[0]["WallClock_DateTime"].isoformat() == (
164 "2023-01-01T01:00:00+13:00"
165 )
166 assert time_index.iloc[-1]["WallClock_DateTime"].isoformat() == (
167 "2024-01-01T00:00:00+13:00"
168 )
169 summer_peak = time_index[
170 (time_index["Month"] == 1)
171 & (time_index["Day"] == 2)
172 & (time_index["Hour"] == 18)
173 ].iloc[0]
174 winter_peak = time_index[
175 (time_index["Month"] == 7)
176 & (time_index["Day"] == 2)
177 & (time_index["Hour"] == 18)
178 ].iloc[0]
179 assert summer_peak["WallClock_Hour"] == 19
180 assert summer_peak["WallClock_UtcOffsetHours"] == 13.0
181 assert winter_peak["WallClock_Hour"] == 18
182 assert winter_peak["WallClock_UtcOffsetHours"] == 12.0
185def test_convert_epw_standard_time_to_wallclock_uses_nz_timezone_rules():
186 """
187 Solar wall-clock conversion should rely on Pacific/Auckland timezone rules.
188 """
189 module = load_solar_run_hourly_profiles()
191 summer = module.convert_epw_standard_time_to_wallclock(1, 1, 18)
192 winter = module.convert_epw_standard_time_to_wallclock(7, 1, 18)
193 dst_start = module.convert_epw_standard_time_to_wallclock(9, 24, 2)
195 assert summer.isoformat() == "2023-01-01T19:00:00+13:00"
196 assert winter.isoformat() == "2023-07-01T18:00:00+12:00"
197 assert dst_start.isoformat() == "2023-09-24T03:00:00+13:00"
200def test_build_time_index_rejects_leap_base_year(tmp_path):
201 """
202 Leap-year base calendars should fail until the workflow handles them explicitly.
203 """
204 module = load_solar_run_hourly_profiles()
206 epw_files = {}
207 for zone in module.ZONE_ORDER:
208 path = tmp_path / f"TMY_NZ_{zone}.epw"
209 write_test_epw(path, 2007)
210 epw_files[zone] = path
212 original_base_year = module.BASE_YEAR
213 module.BASE_YEAR = 2024
214 try:
215 try:
216 module.build_time_index(epw_files)
217 raise AssertionError("Expected leap-year base-year guard to raise")
218 except ValueError as exc:
219 assert "BASE_YEAR 2024 is a leap year" in str(exc)
220 finally:
221 module.BASE_YEAR = original_base_year