Coverage for aixweather/project_class.py: 88%

136 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-12-31 11:58 +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 in UTC 

48 (sometimes inferred by the inheriting class). 

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

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, export_in_utc: bool = False) -> 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 export_in_utc (bool): Timezone to be used for the export. 

157 True (default) to use the core_df timezone, UTC+0, 

158 False (default) to use timezone from metadata 

159 

160 Returns: 

161 str: Path to the exported file. 

162 """ 

163 self.output_data_df, filepath = to_mos( 

164 core_df=self.core_data, 

165 meta=self.meta_data, 

166 start=self.start, 

167 stop=self.end, 

168 fillna=self.fillna, 

169 result_folder=self.abs_result_folder_path, 

170 filename=filename, 

171 export_in_utc=export_in_utc 

172 ) 

173 return filepath 

174 

175 def core_2_epw(self, filename: str = None, export_in_utc: bool = False) -> str: 

176 """ 

177 Convert core data to .epw file 

178 

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

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

181 export_in_utc (bool): Timezone to be used for the export. 

182 True (default) to use the core_df timezone, UTC+0, 

183 False (default) to use timezone from metadata 

184 

185 Returns: 

186 str: Path to the exported file. 

187 """ 

188 self.output_data_df, filepath = to_epw( 

189 self.core_data, 

190 self.meta_data, 

191 self.start, 

192 self.end, 

193 self.fillna, 

194 self.abs_result_folder_path, 

195 filename=filename, 

196 export_in_utc=export_in_utc 

197 ) 

198 return filepath 

199 

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

201 """ 

202 Convert core data to .csv file 

203 

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

205 based on the station name. 

206 

207 Returns: 

208 str: Path to the exported file. 

209 """ 

210 self.output_data_df, filepath = to_csv( 

211 self.core_data, self.meta_data, self.abs_result_folder_path, 

212 filename=filename 

213 ) 

214 return filepath 

215 

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

217 """ 

218 Convert core data to .json file 

219 

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

221 based on the station name. 

222 

223 Returns: 

224 str: Path to the exported file. 

225 """ 

226 self.output_data_df, filepath = to_json( 

227 self.core_data, self.meta_data, self.abs_result_folder_path, 

228 filename=filename 

229 ) 

230 return filepath 

231 

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

233 """ 

234 Convert core data pickle file 

235 

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

237 based on the station name. 

238 

239 Returns: 

240 str: Path to the exported file. 

241 """ 

242 self.output_data_df, filepath = to_pickle( 

243 self.core_data, self.meta_data, self.abs_result_folder_path, 

244 filename=filename 

245 ) 

246 return filepath 

247 

248 

249class ProjectClassDWDHistorical(ProjectClassGeneral): 

250 """ 

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

252 from DWD (Deutscher Wetterdienst). 

253 

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

255 

256 Attributes: 

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

258 """ 

259 

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

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

262 self.station = str(station) 

263 

264 # imports 

265 def import_data(self): 

266 """override abstract function""" 

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

268 self.imported_data = import_DWD_historical( 

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

270 ) # pull more data for better interpolation 

271 

272 # transformation_2_core_data_DWD_Historical 

273 def data_2_core_data(self): 

274 """override abstract function""" 

275 self.core_data = DWD_historical_to_core_data( 

276 self.imported_data, 

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

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

279 self.meta_data, 

280 ) 

281 

282 

283class ProjectClassDWDForecast(ProjectClassGeneral): 

284 """ 

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

286 from DWD (Deutscher Wetterdienst). 

287 

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

289 

290 Attributes: 

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

292 """ 

293 

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

295 super().__init__(**kwargs) 

296 self.station = str(station) 

297 

298 # imports 

299 def import_data(self): 

300 """override abstract function""" 

301 self.meta_data = import_meta_DWD_forecast(self.station) 

302 self.imported_data = import_DWD_forecast(self.station) 

303 

304 def data_2_core_data(self): 

305 """override abstract function""" 

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

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

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

309 

310 

311class ProjectClassERC(ProjectClassGeneral): 

312 """ 

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

314 from the ERC (Energy Research Center). 

315 

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

317 

318 Attributes: 

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

320 the data source. 

321 """ 

322 

323 def __init__( 

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

325 ): 

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

327 self.cred = cred 

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

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

330 

331 def import_data(self): 

332 """override abstract function""" 

333 self.meta_data = import_meta_from_ERC() 

334 self.imported_data = import_ERC( 

335 self.start_hour_earlier, self.end_hour_later, self.cred 

336 ) 

337 

338 def data_2_core_data(self): 

339 """override abstract function""" 

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

341 

342 

343class ProjectClassTRY(ProjectClassGeneral): 

344 """ 

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

346 from TRY (Test Reference Year) format. 

347 

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

349 

350 Attributes: 

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

352 """ 

353 

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

355 super().__init__(**kwargs) 

356 self.path = path 

357 

358 # imports 

359 def import_data(self): 

360 """override abstract function""" 

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

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

363 

364 # transformation_2_core_data_TRY 

365 def data_2_core_data(self): 

366 """override abstract function""" 

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

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

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

370 

371 

372class ProjectClassEPW(ProjectClassGeneral): 

373 """ 

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

375 from EPW (EnergyPlus Weather) format. 

376 

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

378 

379 Attributes: 

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

381 """ 

382 

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

384 super().__init__(**kwargs) 

385 self.path = path 

386 

387 # imports 

388 def import_data(self): 

389 """override abstract function""" 

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

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

392 

393 # transformation_2_core_data_TRY 

394 def data_2_core_data(self): 

395 """override abstract function""" 

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

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

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

399 

400 

401class ProjectClassCustom(ProjectClassGeneral): 

402 """ 

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

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

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

406 

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

408 

409 Attributes: 

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

411 """ 

412 

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

414 super().__init__(**kwargs) 

415 self.path = path 

416 

417 # imports 

418 def import_data(self): 

419 """override abstract function""" 

420 self.meta_data = load_custom_meta_data() 

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

422 

423 # transformation_2_core_data_TRY 

424 def data_2_core_data(self): 

425 """override abstract function""" 

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

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

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