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

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 

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 ) 

89 

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

91 for row in header + rows: 

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

93 

94 

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 ) 

108 

109 result = module.create_timeslices(df) 

110 

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

112 "SUM-WK-P", 

113 "SUM-WE-P", 

114 "WIN-WK-D", 

115 "SPR-WE-N", 

116 ] 

117 

118 

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 ) 

130 

131 result = module.create_timeslices( 

132 df, 

133 date_col="WallClock_Date", 

134 hour_col="WallClock_Hour", 

135 ) 

136 

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

138 

139 

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() 

147 

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 

157 

158 time_index = module.build_time_index(epw_files) 

159 

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 

183 

184 

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() 

190 

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) 

194 

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" 

198 

199 

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() 

205 

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 

211 

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