Coverage for aixweather/project_class.py: 88%

136 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-01-06 16:01 +0000

1"""This module contains the central project classes which are used by the user.""" 

2 

3from abc import ABC, abstractmethod 

4import datetime as dt 

5import pandas as pd 

6 

7from aixweather.imports.DWD import ( 

8 import_DWD_historical, 

9 import_DWD_forecast, 

10 import_meta_DWD_historical, 

11 import_meta_DWD_forecast, 

12) 

13from aixweather.imports.ERC import import_ERC, import_meta_from_ERC 

14from aixweather.imports.TRY import load_try_from_file, load_try_meta_from_file 

15from aixweather.imports.EPW import load_epw_from_file, load_epw_meta_from_file 

16from aixweather.imports.custom_file import load_custom_meta_data, load_custom_from_file 

17from aixweather.transformation_to_core_data.DWD import ( 

18 DWD_historical_to_core_data, 

19 DWD_forecast_2_core_data, 

20) 

21from aixweather.transformation_to_core_data.ERC import ERC_to_core_data 

22from aixweather.transformation_to_core_data.TRY import TRY_to_core_data 

23from aixweather.transformation_to_core_data.EPW import EPW_to_core_data 

24from aixweather.transformation_to_core_data.custom_file import custom_to_core_data 

25from aixweather.core_data_format_2_output_file.unconverted_to_x import ( 

26 to_pickle, 

27 to_json, 

28 to_csv, 

29) 

30from aixweather.core_data_format_2_output_file.to_mos_TMY3 import to_mos 

31from aixweather.core_data_format_2_output_file.to_epw_energyplus import to_epw 

32 

33 

34# pylint-disable: R0902 

35class ProjectClassGeneral(ABC): 

36 """ 

37 An abstract base class representing a general project. 

38 

39 For each source of weather data, a project class should inherit from this class 

40 and implement specific methods for data import and transformation. 

41 

42 Attributes: 

43 fillna (bool): A flag indicating whether NaN values should be filled 

44 in the output formats. 

45 abs_result_folder_path (str): Optionally define the absolute path to 

46 the desired export location. 

47 start (pd.Timestamp or None): The start date of the project data 

48 (sometimes inferred by the inheriting class). 

49 end (pd.Timestamp or None): The end date of the project data. 

50 

51 Properties: 

52 imported_data (pd.DataFrame): The imported weather data. 

53 core_data (pd.DataFrame): The weather data in a standardized core 

54 format. 

55 output_df_<outputformat> (pd.DataFrame): The output data frame 

56 (name depends on output format). 

57 meta_data: Metadata associated with weather data origin. 

58 

59 Methods: 

60 import_data(): An abstract method to import data from the specific 

61 source. 

62 data_2_core_data(): An abstract method to transform imported data into 

63 core data format. 

64 core_2_mos(): Convert core data to MOS format. 

65 core_2_epw(): Convert core data to EPW format. 

66 core_2_csv(): Convert core data to CSV format. 

67 core_2_json(): Convert core data to JSON format. 

68 core_2_pickle(): Convert core data to Pickle format. 

69 """ 

70 

71 def __init__(self, **kwargs): 

72 # User-settable attributes 

73 self.fillna = kwargs.get( 

74 "fillna", True 

75 ) # defines whether nan should be filled in the output formats 

76 self.abs_result_folder_path = kwargs.get("abs_result_folder_path", None) 

77 

78 # User-settable or placeholder depending on data origin 

79 self.start = kwargs.get("start", None) 

80 self.end = kwargs.get("end", None) 

81 

82 # Placeholder attributes (set during execution) 

83 self.imported_data: pd.DataFrame = None 

84 self.core_data: pd.DataFrame = None 

85 self.output_data_df: pd.DataFrame = None 

86 self.meta_data = None 

87 

88 # checks too wordy with getter and setters 

89 self.start_end_checks() 

90 

91 def start_end_checks(self): 

92 """Make sure start and end are of type datetime and end is after start.""" 

93 if self.start is not None and self.end is not None: 

94 if not isinstance(self.start, dt.datetime) or not isinstance(self.end, dt.datetime): 

95 raise ValueError("Time period for pulling data: start and end must be of " 

96 "type datetime") 

97 # make sure end is after start 

98 if self.end < self.start: 

99 raise ValueError("Time period for pulling data: end must be after start") 

100 

101 @property 

102 def imported_data(self): 

103 """Get imported data""" 

104 return self._imported_data 

105 

106 @imported_data.setter 

107 def imported_data(self, data): 

108 """If the imported data is empty, the program should be stopped with clear error 

109 description.""" 

110 if isinstance(data, pd.DataFrame) and data.empty: 

111 raise ValueError( 

112 "Imported data cannot be an empty DataFrame. No weather data " 

113 "has been found. Possible reasons are: " 

114 "The station id is wrong or the station does not exist/measure " 

115 "anymore, the data is currently not available or the weather " 

116 "provider can not be accessed.\n" 

117 f"The so far pulled meta data is: {self.meta_data.__dict__}" 

118 ) 

119 self._imported_data = data 

120 

121 @property 

122 def core_data(self): 

123 """Get core data""" 

124 return self._core_data 

125 

126 @core_data.setter 

127 def core_data(self, value: pd.DataFrame): 

128 """Makes sure the core-data data types are correct.""" 

129 if value is not None: 

130 for column in value.columns: 

131 # only real pd.NA values 

132 # force strings to be NaN 

133 value[column] = pd.to_numeric(value[column], errors="coerce") 

134 # round floats for unit test compatibility across different machines 

135 digits_2_round = 5 

136 if value[column].dtype == "float": 

137 value[column] = value[column].round(digits_2_round) 

138 

139 self._core_data = value 

140 

141 @abstractmethod 

142 def import_data(self): 

143 """Abstract function to import weather data.""" 

144 

145 @abstractmethod 

146 def data_2_core_data(self): 

147 """Abstract function to convert the imported data to core data.""" 

148 

149 # core_data_format_2_output_file 

150 def core_2_mos(self, filename: str = None) -> str: 

151 """ 

152 Convert core data to .mos file 

153 

154 filename (str): Name of the file to be saved. The default is constructed 

155 based on the meta-data as well as start and stop time 

156 

157 Returns: 

158 str: Path to the exported file. 

159 """ 

160 self.output_data_df, filepath = to_mos( 

161 self.core_data, 

162 self.meta_data, 

163 self.start, 

164 self.end, 

165 self.fillna, 

166 self.abs_result_folder_path, 

167 filename=filename 

168 ) 

169 return filepath 

170 

171 def core_2_epw(self, filename: str = None) -> str: 

172 """ 

173 Convert core data to .epw file 

174 

175 filename (str): Name of the file to be saved. The default is constructed 

176 based on the meta-data as well as start and stop time 

177 

178 Returns: 

179 str: Path to the exported file. 

180 """ 

181 self.output_data_df, filepath = to_epw( 

182 self.core_data, 

183 self.meta_data, 

184 self.start, 

185 self.end, 

186 self.fillna, 

187 self.abs_result_folder_path, 

188 filename=filename 

189 ) 

190 return filepath 

191 

192 def core_2_csv(self, filename: str = None) -> str: 

193 """ 

194 Convert core data to .csv file 

195 

196 filename (str): Name of the file to be saved. The default is constructed 

197 based on the station name. 

198 

199 Returns: 

200 str: Path to the exported file. 

201 """ 

202 self.output_data_df, filepath = to_csv( 

203 self.core_data, self.meta_data, self.abs_result_folder_path, 

204 filename=filename 

205 ) 

206 return filepath 

207 

208 def core_2_json(self, filename: str = None) -> str: 

209 """ 

210 Convert core data to .json file 

211 

212 filename (str): Name of the file to be saved. The default is constructed 

213 based on the station name. 

214 

215 Returns: 

216 str: Path to the exported file. 

217 """ 

218 self.output_data_df, filepath = to_json( 

219 self.core_data, self.meta_data, self.abs_result_folder_path, 

220 filename=filename 

221 ) 

222 return filepath 

223 

224 def core_2_pickle(self, filename: str = None) -> str: 

225 """ 

226 Convert core data pickle file 

227 

228 filename (str): Name of the file to be saved. The default is constructed 

229 based on the station name. 

230 

231 Returns: 

232 str: Path to the exported file. 

233 """ 

234 self.output_data_df, filepath = to_pickle( 

235 self.core_data, self.meta_data, self.abs_result_folder_path, 

236 filename=filename 

237 ) 

238 return filepath 

239 

240 

241class ProjectClassDWDHistorical(ProjectClassGeneral): 

242 """ 

243 A class representing a project for importing and processing historical weather data 

244 from DWD (Deutscher Wetterdienst). 

245 

246 For common attributes, properties, and methods, refer to the base class. 

247 

248 Attributes: 

249 station (str): The identifier of the DWD weather station associated with the data. 

250 """ 

251 

252 def __init__(self, start: dt.datetime, end: dt.datetime, station: str, **kwargs): 

253 super().__init__(start=start, end=end, **kwargs) 

254 self.station = str(station) 

255 

256 # imports 

257 def import_data(self): 

258 """override abstract function""" 

259 self.meta_data = import_meta_DWD_historical(station=self.station) 

260 self.imported_data = import_DWD_historical( 

261 self.start - dt.timedelta(days=1), self.station 

262 ) # pull more data for better interpolation 

263 

264 # transformation_2_core_data_DWD_Historical 

265 def data_2_core_data(self): 

266 """override abstract function""" 

267 self.core_data = DWD_historical_to_core_data( 

268 self.imported_data, 

269 self.start - dt.timedelta(days=1), 

270 self.end + dt.timedelta(days=1), 

271 self.meta_data, 

272 ) 

273 

274 

275class ProjectClassDWDForecast(ProjectClassGeneral): 

276 """ 

277 A class representing a project for importing and processing weather forecast data 

278 from DWD (Deutscher Wetterdienst). 

279 

280 For common attributes, properties, and methods, refer to the base class. 

281 

282 Attributes: 

283 station (str): The identifier of the KML grid associated with the forecast data. 

284 """ 

285 

286 def __init__(self, station: str, **kwargs): 

287 super().__init__(**kwargs) 

288 self.station = str(station) 

289 

290 # imports 

291 def import_data(self): 

292 """override abstract function""" 

293 self.meta_data = import_meta_DWD_forecast(self.station) 

294 self.imported_data = import_DWD_forecast(self.station) 

295 

296 def data_2_core_data(self): 

297 """override abstract function""" 

298 self.core_data = DWD_forecast_2_core_data(self.imported_data, self.meta_data) 

299 self.start = self.core_data.index[0] 

300 self.end = self.core_data.index[-1] 

301 

302 

303class ProjectClassERC(ProjectClassGeneral): 

304 """ 

305 A class representing a project for importing and processing weather data 

306 from the ERC (Energy Research Center). 

307 

308 For common attributes, properties, and methods, refer to the base class. 

309 

310 Attributes: 

311 cred (tuple): A tuple containing credentials or authentication information for accessing 

312 the data source. 

313 """ 

314 

315 def __init__( 

316 self, start: dt.datetime, end: dt.datetime, cred: tuple = None, **kwargs 

317 ): 

318 super().__init__(start=start, end=end, **kwargs) 

319 self.cred = cred 

320 self.start_hour_earlier = start - dt.timedelta(hours=2) 

321 self.end_hour_later = end + dt.timedelta(hours=2) 

322 

323 def import_data(self): 

324 """override abstract function""" 

325 self.meta_data = import_meta_from_ERC() 

326 self.imported_data = import_ERC( 

327 self.start_hour_earlier, self.end_hour_later, self.cred 

328 ) 

329 

330 def data_2_core_data(self): 

331 """override abstract function""" 

332 self.core_data = ERC_to_core_data(self.imported_data, self.meta_data) 

333 

334 

335class ProjectClassTRY(ProjectClassGeneral): 

336 """ 

337 A class representing a project for importing and processing weather data 

338 from TRY (Test Reference Year) format. 

339 

340 For common attributes, properties, and methods, refer to the base class. 

341 

342 Attributes: 

343 path (str): The absolute file path to the TRY weather data. 

344 """ 

345 

346 def __init__(self, path, **kwargs): 

347 super().__init__(**kwargs) 

348 self.path = path 

349 

350 # imports 

351 def import_data(self): 

352 """override abstract function""" 

353 self.meta_data = load_try_meta_from_file(path=self.path) 

354 self.imported_data = load_try_from_file(path=self.path) 

355 

356 # transformation_2_core_data_TRY 

357 def data_2_core_data(self): 

358 """override abstract function""" 

359 self.core_data = TRY_to_core_data(self.imported_data, self.meta_data) 

360 self.start = self.core_data.index[0] 

361 self.end = self.core_data.index[-1] 

362 

363 

364class ProjectClassEPW(ProjectClassGeneral): 

365 """ 

366 A class representing a project for importing and processing weather data 

367 from EPW (EnergyPlus Weather) format. 

368 

369 For common attributes, properties, and methods, refer to the base class. 

370 

371 Attributes: 

372 path (str): The absolute file path to the EPW weather data. 

373 """ 

374 

375 def __init__(self, path, **kwargs): 

376 super().__init__(**kwargs) 

377 self.path = path 

378 

379 # imports 

380 def import_data(self): 

381 """override abstract function""" 

382 self.meta_data = load_epw_meta_from_file(path=self.path) 

383 self.imported_data = load_epw_from_file(path=self.path) 

384 

385 # transformation_2_core_data_TRY 

386 def data_2_core_data(self): 

387 """override abstract function""" 

388 self.core_data = EPW_to_core_data(self.imported_data, self.meta_data) 

389 self.start = self.core_data.index[0] 

390 self.end = self.core_data.index[-1] 

391 

392 

393class ProjectClassCustom(ProjectClassGeneral): 

394 """ 

395 A class representing a project for importing and processing custom weather data. 

396 Modify this class and its functions to create your own weather data pipeline 

397 and consider to create a pull request to add the pipeline to the repository. 

398 

399 For common attributes, properties, and methods, refer to the base class. 

400 

401 Attributes: 

402 path (str): The file path to the custom weather data. 

403 """ 

404 

405 def __init__(self, path, **kwargs): 

406 super().__init__(**kwargs) 

407 self.path = path 

408 

409 # imports 

410 def import_data(self): 

411 """override abstract function""" 

412 self.meta_data = load_custom_meta_data() 

413 self.imported_data = load_custom_from_file(path=self.path) 

414 

415 # transformation_2_core_data_TRY 

416 def data_2_core_data(self): 

417 """override abstract function""" 

418 self.core_data = custom_to_core_data(self.imported_data, self.meta_data) 

419 self.start = self.core_data.index[0] # or define in init 

420 self.end = self.core_data.index[-1] # or define in init