Coverage for tests/test_timeslices.py: 99%

96 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-16 23:05 +0000

1"""Tests for solar-script timeslice helpers.""" 

2 

3import importlib.util 

4from pathlib import Path 

5 

6import pandas as pd 

7 

8 

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] 

21 

22[ActivePDef.Data] 

23ActivePDef = ["5Year_increments"] 

24""".strip() + "\n", 

25 encoding="utf-8", 

26 ) 

27 

28 

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 

45 

46 

47# pylint: disable=too-many-arguments,too-many-positional-arguments 

48def write_test_epw( 

49 path: Path, 

50 header_year: int, 

51 start_weekday: str = "Sunday", 

52 days_in_year: int = 365, 

53 calendar_label: str = "TMY3 Year", 

54 minute: str = "60", 

55 data_period_col_6: str | None = None, 

56 data_period_col_7: str | None = None, 

57): 

58 """ 

59 Write a minimal synthetic EPW file for calendar and timeslice tests. 

60 """ 

61 header = [ 

62 [ 

63 "LOCATION", 

64 "Test", 

65 "Test", 

66 "New Zealand", 

67 "TMY3", 

68 "0", 

69 "0", 

70 "0", 

71 "0", 

72 "0", 

73 ], 

74 ["DESIGN CONDITIONS", "0"], 

75 ["TYPICAL/EXTREME PERIODS", "0"], 

76 ["GROUND TEMPERATURES", "0"], 

77 ["HOLIDAYS/DAYLIGHT SAVING", "No", "0", "0", "0"], 

78 ["COMMENTS 1", "Synthetic test EPW"], 

79 ["COMMENTS 2", "Synthetic test EPW"], 

80 [ 

81 "DATA PERIODS", 

82 "1", 

83 "1", 

84 calendar_label, 

85 start_weekday, 

86 data_period_col_6 if data_period_col_6 is not None else "1", 

87 data_period_col_7 if data_period_col_7 is not None else str(days_in_year), 

88 ], 

89 ] 

90 rows = [] 

91 for day in pd.date_range(f"{header_year}-01-01", f"{header_year}-12-31", freq="D"): 

92 for hour in range(1, 25): 

93 rows.append( 

94 [ 

95 f"{day.year:04d}", 

96 f"{day.month:02d}", 

97 f"{day.day:02d}", 

98 f"{hour:02d}", 

99 minute, 

100 ] 

101 ) 

102 

103 with path.open("w", encoding="utf-8", newline="") as handle: 

104 for row in header + rows: 

105 handle.write(",".join(row) + "\n") 

106 

107 

108def test_create_timeslices_uses_project_time_of_day_and_daytype_definitions(): 

109 """ 

110 Shared timeslice construction should match the active repo settings. 

111 """ 

112 module = load_solar_run_hourly_profiles() 

113 df = pd.DataFrame( 

114 { 

115 "Trading_Date": pd.to_datetime( 

116 ["2023-01-02", "2023-01-07", "2023-07-03", "2023-09-10"] 

117 ), 

118 "Hour": [18, 18, 7, 19], 

119 } 

120 ) 

121 

122 result = module.create_timeslices(df) 

123 

124 assert result["TimeSlice"].tolist() == [ 

125 "SUM-WK-P", 

126 "SUM-WE-P", 

127 "WIN-WK-D", 

128 "SPR-WE-N", 

129 ] 

130 

131 

132def test_create_timeslices_can_use_wall_clock_hour_column(): 

133 """ 

134 Timeslice construction should support solar wall-clock hours. 

135 """ 

136 module = load_solar_run_hourly_profiles() 

137 df = pd.DataFrame( 

138 { 

139 "WallClock_Date": pd.to_datetime(["2023-01-02", "2023-07-03"]), 

140 "WallClock_Hour": [19, 18], 

141 } 

142 ) 

143 

144 result = module.create_timeslices( 

145 df, 

146 date_col="WallClock_Date", 

147 hour_col="WallClock_Hour", 

148 ) 

149 

150 assert result["TimeSlice"].tolist() == ["SUM-WK-N", "WIN-WK-P"] 

151 

152 

153def test_build_time_index_uses_model_base_year_and_ignores_epw_calendar_metadata( 

154 tmp_path, 

155): 

156 """ 

157 Solar timeslices should use the model base year rather than the EPW calendar. 

158 """ 

159 module = load_solar_run_hourly_profiles() 

160 

161 epw_files = {} 

162 for zone in module.ZONE_ORDER: 

163 path = tmp_path / f"TMY3_NZ_{zone}.epw" 

164 write_test_epw( 

165 path, 

166 1999 if zone == "AK" else 2007, 

167 start_weekday="Monday", 

168 ) 

169 epw_files[zone] = path 

170 

171 time_index = module.build_time_index(epw_files) 

172 

173 assert time_index["Trading_Date"].min() == pd.Timestamp("2023-01-01") 

174 assert time_index["Trading_Date"].max() == pd.Timestamp("2023-12-31") 

175 assert time_index.iloc[0]["Trading_Date"].day_name() == "Sunday" 

176 assert time_index.iloc[0]["WallClock_DateTime"].isoformat() == ( 

177 "2023-01-01T01:00:00+13:00" 

178 ) 

179 assert time_index.iloc[-1]["WallClock_DateTime"].isoformat() == ( 

180 "2024-01-01T00:00:00+13:00" 

181 ) 

182 summer_peak = time_index[ 

183 (time_index["Month"] == 1) 

184 & (time_index["Day"] == 2) 

185 & (time_index["Hour"] == 18) 

186 ].iloc[0] 

187 winter_peak = time_index[ 

188 (time_index["Month"] == 7) 

189 & (time_index["Day"] == 2) 

190 & (time_index["Hour"] == 18) 

191 ].iloc[0] 

192 assert summer_peak["WallClock_Hour"] == 19 

193 assert summer_peak["WallClock_UtcOffsetHours"] == 13.0 

194 assert winter_peak["WallClock_Hour"] == 18 

195 assert winter_peak["WallClock_UtcOffsetHours"] == 12.0 

196 

197 

198def test_convert_epw_standard_time_to_wallclock_uses_nz_timezone_rules(): 

199 """ 

200 Solar wall-clock conversion should rely on Pacific/Auckland timezone rules. 

201 """ 

202 module = load_solar_run_hourly_profiles() 

203 

204 summer = module.convert_epw_standard_time_to_wallclock(1, 1, 18) 

205 winter = module.convert_epw_standard_time_to_wallclock(7, 1, 18) 

206 dst_start = module.convert_epw_standard_time_to_wallclock(9, 24, 2) 

207 

208 assert summer.isoformat() == "2023-01-01T19:00:00+13:00" 

209 assert winter.isoformat() == "2023-07-01T18:00:00+12:00" 

210 assert dst_start.isoformat() == "2023-09-24T03:00:00+13:00" 

211 

212 

213def test_build_time_index_rejects_leap_base_year(tmp_path): 

214 """ 

215 Leap-year base calendars should fail until the workflow handles them explicitly. 

216 """ 

217 module = load_solar_run_hourly_profiles() 

218 

219 epw_files = {} 

220 for zone in module.ZONE_ORDER: 

221 path = tmp_path / f"TMY3_NZ_{zone}.epw" 

222 write_test_epw(path, 2007) 

223 epw_files[zone] = path 

224 

225 original_base_year = module.BASE_YEAR 

226 module.BASE_YEAR = 2024 

227 try: 

228 try: 

229 module.build_time_index(epw_files) 

230 raise AssertionError("Expected leap-year base-year guard to raise") 

231 except ValueError as exc: 

232 assert "BASE_YEAR 2024 is a leap year" in str(exc) 

233 finally: 

234 module.BASE_YEAR = original_base_year 

235 

236 

237def test_discover_epw_files_accepts_tmy3_names(tmp_path): 

238 """ 

239 Prepared EPW discovery should work for the committed TMY3 filenames. 

240 """ 

241 module = load_solar_run_hourly_profiles() 

242 

243 for zone in module.ZONE_ORDER: 

244 write_test_epw(tmp_path / f"TMY3_NZ_{zone}.epw", 2024) 

245 

246 discovered = module.discover_epw_files(tmp_path) 

247 

248 assert set(discovered) == set(module.ZONE_ORDER) 

249 assert discovered["AK"].name == "TMY3_NZ_AK.epw" 

250 assert discovered["HN"].name == "TMY3_NZ_HN.epw" 

251 

252 

253def test_parse_epw_data_period_metadata_accepts_tmy3_header_dates(tmp_path): 

254 """ 

255 MBIE TMY3 headers use start/end dates instead of a numeric day count. 

256 """ 

257 module = load_solar_run_hourly_profiles() 

258 path = tmp_path / "TMY3_NZ_AK.epw" 

259 write_test_epw( 

260 path, 

261 2024, 

262 minute="0", 

263 data_period_col_6="1/ 1", 

264 data_period_col_7="12/31", 

265 ) 

266 

267 metadata = module.parse_epw_data_period_metadata(path) 

268 

269 assert metadata["calendar_label"] == "TMY3 Year" 

270 assert metadata["days_in_year"] == 365 

271 assert metadata["start_date"] == "1/ 1" 

272 assert metadata["end_date"] == "12/31"