Coverage for teaser/data/output/besmod_output.py: 99%

164 statements  

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

1"""This module contains function for BESMod model generation""" 

2 

3import os 

4import warnings 

5from typing import Optional, Union, List, Dict 

6from mako.template import Template 

7from mako.lookup import TemplateLookup 

8import teaser.logic.utilities as utilities 

9import teaser.data.output.modelica_output as modelica_output 

10from teaser.logic.buildingobjects.building import Building 

11 

12 

13def export_besmod( 

14 buildings: Union[List[Building], Building], 

15 prj: 'Project', 

16 path: Optional[str] = None, 

17 examples: Optional[List[str]] = None, 

18 THydSup_nominal: Optional[Union[float, Dict[str, float]]] = None, 

19 QBuiOld_flow_design: Optional[Dict[str, Dict[str, float]]] = None, 

20 THydSupOld_design: Optional[Union[float, Dict[str, float]]] = None, 

21 custom_examples: Optional[Dict[str, str]] = None, 

22 custom_script: Optional[Dict[str, str]] = None 

23) -> None: 

24 """ 

25 Export building models for BESMod simulations. 

26 

27 This function generates BESMod.Systems.Demand.Building.TEASERThermalZone models 

28 for one or more TEASER buildings. It also allows exporting examples from 

29 BESMod.Examples, including the building models. 

30 

31 Parameters 

32 ---------- 

33 buildings : Union[List[Building], Building] 

34 TEASER Building instances to export as BESMod models. Can be a single 

35 Building or a list of Buildings. 

36 prj : Project 

37 TEASER Project instance containing project metadata such as library 

38 versions and weather file paths. 

39 examples : Optional[List[str]] 

40 Names of BESMod examples to export alongside the building models. 

41 Supported Examples: "TEASERHeatLoadCalculation", "HeatPumpMonoenergetic", and "GasBoilerBuildingOnly". 

42 path : Optional[str] 

43 Alternative output path for storing the exported files. If None, the default TEASER output path is used. 

44 THydSup_nominal : Optional[Union[float, Dict[str, float]]] 

45 Nominal supply temperature(s) for the hydraulic system. Required for 

46 certain examples (e.g., HeatPumpMonoenergetic, GasBoilerBuildingOnly). 

47 See docstring of teaser.data.output.besmod_output.convert_input() for further information. 

48 QBuiOld_flow_design : Optional[Dict[str, Dict[str, float]]] 

49 For partially retrofitted systems specify the old nominal heat flow 

50 of all zones in the Buildings in a nested dictionary with 

51 the building names and in a level below the zone names as keys. 

52 By default, only the radiator transfer system is not retrofitted in BESMod. 

53 THydSupOld_design : Optional[Union[float, Dict[str, float]]] 

54 Design supply temperatures for old, non-retrofitted hydraulic systems. 

55 custom_examples: Optional[Dict[str, str]] 

56 Specify custom examples with a dictionary containing the example name as the key and 

57 the path to the corresponding custom mako template as the value. 

58 custom_script: Optional[Dict[str, str]] 

59 Specify custom .mos scripts for the existing and custom examples with a dictionary 

60 containing the example name as the key and the path to the corresponding custom mako template as the value. 

61 

62 Raises 

63 ------ 

64 ValueError 

65 If given example is not supported. 

66 ValueError 

67 If `THydSup_nominal` is not provided for examples requiring it. 

68 AssertionError 

69 If the used library for calculations is not AixLib. 

70 NotImplementedError 

71 If a building uses a thermal zone model other than the four-element model. 

72 

73 Notes 

74 ----- 

75 The function uses Mako templates for generating Modelica models. 

76 """ 

77 

78 if prj.used_library_calc != "AixLib": 

79 raise AttributeError("BESMod export is only implemented for AixLib calculation.") 

80 

81 if examples is None: 

82 examples = [] 

83 

84 if path is None: 

85 path = utilities.get_full_path("") 

86 

87 if not isinstance(examples, list): 

88 examples = [examples] 

89 

90 supported_examples = [ 

91 "TEASERHeatLoadCalculation", 

92 "HeatPumpMonoenergetic", 

93 "GasBoilerBuildingOnly", 

94 ] 

95 

96 for exp in examples: 

97 if exp not in supported_examples: 

98 raise ValueError( 

99 f"Example {exp} is not supported. " 

100 f"Supported examples are {supported_examples}." 

101 ) 

102 

103 if THydSup_nominal is None and any( 

104 example in examples for example in ["HeatPumpMonoenergetic", "GasBoilerBuildingOnly"] 

105 ): 

106 raise ValueError( 

107 "Examples 'HeatPumpMonoenergetic' and 'GasBoilerBuildingOnly' " 

108 "require the `THydSup_nominal` parameter." 

109 ) 

110 elif THydSup_nominal is None: 

111 THydSup_nominal = 328.15 

112 if custom_examples: 

113 warnings.warn("If you set THydSup_nominal in your custom examples template, " 

114 "please provide it in the export. " 

115 "Otherwise, the default value of 328.15 K will be used.") 

116 

117 t_hyd_sup_nominal_bldg = convert_input(THydSup_nominal, buildings) 

118 t_hyd_sup_old_design_bldg = ( 

119 convert_input(THydSupOld_design, buildings) 

120 if THydSupOld_design 

121 else {bldg.name: "systemParameters.THydSup_nominal" for bldg in buildings} 

122 ) 

123 

124 if QBuiOld_flow_design is None: 

125 QBuiOld_flow_design = { 

126 bldg.name: "systemParameters.QBui_flow_nominal" for bldg in buildings 

127 } 

128 else: 

129 QBuiOld_flow_design = { 

130 bldg.name: _convert_to_zone_array(bldg, QBuiOld_flow_design[bldg.name]) 

131 for bldg in buildings 

132 } 

133 

134 if custom_script is None: 

135 custom_script = {} 

136 

137 dir_resources = utilities.create_path(os.path.join(path, "Resources")) 

138 dir_scripts = utilities.create_path(os.path.join(dir_resources, "Scripts")) 

139 dir_dymola = utilities.create_path(os.path.join(dir_scripts, "Dymola")) 

140 template_path = utilities.get_full_path("data/output/modelicatemplate") 

141 lookup = TemplateLookup(directories=[template_path]) 

142 

143 zone_template_4 = Template( 

144 filename=os.path.join(template_path, "AixLib/AixLib_ThermalZoneRecord_FourElement"), 

145 lookup=lookup) 

146 building_template = Template( 

147 filename=os.path.join(template_path, "BESMod/Building"), 

148 lookup=lookup) 

149 

150 uses = [ 

151 'Modelica(version="' + prj.modelica_info.version + '")', 

152 'AixLib(version="' + prj.buildings[-1].library_attr.version + '")', 

153 'BESMod(version="' + prj.buildings[-1].library_attr.besmod_version + '")'] 

154 modelica_output.create_package( 

155 path=path, 

156 name=prj.name, 

157 uses=uses) 

158 modelica_output.create_package_order( 

159 path=path, 

160 package_list=buildings) 

161 modelica_output.copy_weather_data(prj.weather_file_path, dir_resources) 

162 

163 for i, bldg in enumerate(buildings): 

164 bldg.bldg_height = bldg.number_of_floors * bldg.height_of_floors 

165 start_time_zones = [] 

166 width_zones = [] 

167 amplitude_zones = [] 

168 t_set_zone_nominal = [] 

169 for tz in bldg.thermal_zones: 

170 heating_profile = tz.use_conditions.heating_profile 

171 t_set_nominal, start_time, width, amplitude = _convert_heating_profile(heating_profile) 

172 t_set_zone_nominal.append(t_set_nominal) 

173 amplitude_zones.append(amplitude) 

174 start_time_zones.append(start_time) 

175 width_zones.append(width) 

176 

177 bldg_path = os.path.join(path, bldg.name) 

178 utilities.create_path(bldg_path) 

179 utilities.create_path(os.path.join(bldg_path, bldg.name + "_DataBase")) 

180 bldg.library_attr.modelica_gains_boundary(path=bldg_path) 

181 

182 with open(os.path.join(bldg_path, bldg.name + ".mo"), 'w') as out_file: 

183 out_file.write(building_template.render_unicode( 

184 bldg=bldg)) 

185 out_file.close() 

186 

187 def write_example_mo(example_template, example): 

188 with open(os.path.join(bldg_path, example + bldg.name + ".mo"), 

189 'w') as model_file: 

190 model_file.write(example_template.render_unicode( 

191 bldg=bldg, 

192 project=prj, 

193 TOda_nominal=bldg.thermal_zones[0].t_outside, 

194 THydSup_nominal=t_hyd_sup_nominal_bldg[bldg.name], 

195 TSetZone_nominal=t_set_zone_nominal, 

196 QBuiOld_flow_design=QBuiOld_flow_design[bldg.name], 

197 THydSupOld_design=t_hyd_sup_old_design_bldg[bldg.name], 

198 setBakTSetZone_amplitude=amplitude_zones, 

199 setBakTSetZone_startTime=start_time_zones, 

200 setBakTSetZone_width=width_zones)) 

201 model_file.close() 

202 

203 for exp in examples: 

204 exp_template = Template( 

205 filename=utilities.get_full_path( 

206 "data/output/modelicatemplate/BESMod/Example_" + exp), 

207 lookup=lookup) 

208 if exp in custom_script.keys(): 

209 example_sim_plot_script = Template( 

210 filename=custom_script[exp], 

211 lookup=lookup) 

212 else: 

213 example_sim_plot_script = Template( 

214 filename=utilities.get_full_path( 

215 "data/output/modelicatemplate/BESMod/Script_" + exp), 

216 lookup=lookup) 

217 _help_example_script(bldg, dir_dymola, example_sim_plot_script, exp) 

218 write_example_mo(exp_template, exp) 

219 bldg_package = [exp + bldg.name for exp in examples] 

220 if custom_examples: 

221 for exp, c_path in custom_examples.items(): 

222 bldg_package.append(exp + bldg.name) 

223 exp_template = Template( 

224 filename=c_path, 

225 lookup=lookup) 

226 write_example_mo(exp_template, exp) 

227 if exp in custom_script.keys(): 

228 example_sim_plot_script = Template( 

229 filename=custom_script[exp], 

230 lookup=lookup) 

231 _help_example_script(bldg, dir_dymola, example_sim_plot_script, exp) 

232 

233 bldg_package.append(bldg.name + "_DataBase") 

234 modelica_output.create_package(path=bldg_path, name=bldg.name, within=bldg.parent.name) 

235 modelica_output.create_package_order( 

236 path=bldg_path, 

237 package_list=[bldg], 

238 extra=bldg_package) 

239 

240 zone_path = os.path.join(bldg_path, bldg.name + "_DataBase") 

241 

242 for zone in bldg.thermal_zones: 

243 zone.use_conditions.with_heating = False 

244 with open(os.path.join( 

245 zone_path, 

246 bldg.name + '_' + zone.name + '.mo'), 'w') as out_file: 

247 if type(zone.model_attr).__name__ == "FourElement": 

248 out_file.write(zone_template_4.render_unicode(zone=zone)) 

249 else: 

250 raise NotImplementedError("BESMod export is only implemented for four elements.") 

251 out_file.close() 

252 

253 modelica_output.create_package( 

254 path=zone_path, 

255 name=bldg.name + '_DataBase', 

256 within=prj.name + '.' + bldg.name) 

257 modelica_output.create_package_order( 

258 path=zone_path, 

259 package_list=bldg.thermal_zones, 

260 addition=bldg.name + "_") 

261 

262 print("Exports can be found here:") 

263 print(path) 

264 

265 

266def convert_input(building_zones_input: Union[float, Dict[Union[int, str], Union[float, Dict[str, float]]]], 

267 buildings: List[Building]) -> Dict[str, str]: 

268 """ 

269 Convert input values for BESMod zone specific parameters to a dictionary. 

270 

271 Supports single values, dictionaries keyed by construction year, or 

272 dictionaries keyed by building names. 

273 If single values are given then all buildings and zones get this values set. 

274 If a dictionary keyed by construction year is given then all zones of a building get the 

275 value set of the next higher year corresponding to the construction year of the building. 

276 If a dictionary keyed by building name is given the value must be a single value for all zones 

277 or another dictionary specifying for each zone name a value. 

278 

279 Parameters 

280 ---------- 

281 building_zones_input : Union[float, Dict[Union[int, str], Union[float, Dict[str, float]]]] 

282 Input value(s) for BESMod parameters. Can be a single value, a dictionary keyed by construction year, 

283 or a dictionary keyed by building name. 

284 Example: 

285 - Single value: 328.15 

286 - Dictionary keyed by construction year: {1970: 348.15, 1990: 328.15} 

287 - Dictionary keyed by building name: { 

288 "Building1": 328.15, 

289 "Building2": { 

290 "Zone1": 328.15, 

291 "Zone2": 308.15 

292 } 

293 } 

294 buildings : List[Building] 

295 List of TEASER Building instances. 

296 

297 Returns 

298 ------- 

299 Dict[str, str] 

300 Dictionary mapping building names to BESMod parameter input strings. 

301 

302 Raises 

303 ------ 

304 ValueError 

305 If the input dictionary has invalid values. 

306 KeyError 

307 If the input dictionary is missing required keys. 

308 """ 

309 bldg_names = [bldg.name for bldg in buildings] 

310 if isinstance(building_zones_input, (float, int)): 

311 return {bldg.name: f"fill({building_zones_input},systemParameters.nZones)" for bldg in buildings} 

312 elif isinstance(building_zones_input, dict): 

313 t_hyd_sup_nominal_bldg = {} 

314 if isinstance(list(building_zones_input.keys())[0], int): 

315 for bldg in buildings: 

316 temperature = _get_next_higher_year_value(building_zones_input, bldg.year_of_construction) 

317 t_hyd_sup_nominal_bldg[bldg.name] = f"fill({temperature},systemParameters.nZones)" 

318 elif set(list(building_zones_input.keys())) == set(bldg_names): 

319 for bldg in buildings: 

320 if isinstance(building_zones_input[bldg.name], (int, float)): 

321 t_hyd_sup_nominal_bldg[ 

322 bldg.name] = f"fill({building_zones_input[bldg.name]},systemParameters.nZones)" 

323 elif isinstance(building_zones_input[bldg.name], dict): 

324 t_hyd_sup_nominal_bldg[bldg.name] = _convert_to_zone_array(bldg, building_zones_input[bldg.name]) 

325 else: 

326 raise ValueError("If THydSup_nominal is specified for all buildings in a dictionary " 

327 "the values must be either a single value for all thermal zones or " 

328 "a dict with all building.thermal_zones.name as keys.") 

329 else: 

330 raise KeyError("If THydSup_nominal is given by a dictionary " 

331 "the keys must be all building names or construction years.") 

332 return t_hyd_sup_nominal_bldg 

333 

334 

335def _convert_to_zone_array(bldg, zone_dict): 

336 """ 

337 Convert a dictionary of zone values to a BESMod-compatible array string. 

338 

339 Parameters 

340 ---------- 

341 bldg : Building 

342 TEASER Building instance. 

343 zone_dict : dict 

344 Dictionary with zone names as keys and zone parameter values as values. 

345 

346 Returns 

347 ------- 

348 str 

349 Array string for BESMod parameter input. 

350 

351 Raises 

352 ------ 

353 KeyError 

354 If the dictionary is missing zone names present in the building. 

355 """ 

356 tz_names = [tz.name for tz in bldg.thermal_zones] 

357 if set(tz_names) == set(list(zone_dict.keys())): 

358 array_string = "{" 

359 for tz in tz_names: 

360 array_string += str(zone_dict[tz]) + "," 

361 return array_string[:-1] + "}" 

362 else: 

363 raise KeyError(f"{set(tz_names) - set(list(zone_dict.keys()))} thermal zones missing in given dictionary.") 

364 

365 

366def _convert_heating_profile(heating_profile): 

367 """ 

368 Convert a 24-hour heating profile for BESMod export. 

369 

370 This function analyzes a 24-hour heating profile to extract: 

371 - The nominal temperature. 

372 - Start time of setbacks (if any). 

373 - Width of setback intervals. 

374 - Amplitude of the heating variation. 

375 

376 Parameters 

377 ---------- 

378 heating_profile : list[float] 

379 List of 24 hourly heating temperatures. 

380 

381 Returns 

382 ------- 

383 t_set_zone_nominal : float 

384 Maximum temperature in the profile, used as the nominal set point. 

385 start_time : int 

386 Start time of the setback interval in seconds. 

387 width : float 

388 Width of the setback interval as a percentage of the day. 

389 amplitude : float 

390 Difference between the minimum and nominal temperatures. 

391 

392 Raises 

393 ------ 

394 ValueError 

395 If the profile has more than two distinct intervals or does not have 24 values. 

396 """ 

397 

398 if len(heating_profile) != 24: 

399 raise ValueError("Only 24 hours heating profiles can be used for BESMod export.") 

400 change_count = 0 

401 change_indexes = [] 

402 for i in range(1, len(heating_profile)): 

403 if heating_profile[i] != heating_profile[i - 1]: 

404 change_count += 1 

405 change_indexes.append(i) 

406 t_set_zone_nominal = max(heating_profile) 

407 amplitude = min(heating_profile) - t_set_zone_nominal 

408 if change_count == 0: 

409 amplitude = 0 

410 start_time = 0 

411 width = 1e-50 

412 elif change_count == 1: 

413 if heating_profile[0] < heating_profile[-1]: 

414 start_time = 0 

415 width = 100 * change_indexes[0] / 24 

416 else: 

417 start_time = change_indexes[0] * 3600 

418 width = 100 * (24 - change_indexes[0]) / 24 

419 elif change_count == 2: 

420 start_time = change_indexes[1] * 3600 

421 width = 100 * (24 - change_indexes[1] + change_indexes[0]) / 24 

422 else: 

423 raise ValueError("You have more than two temperature intervals in the heating profile." 

424 "BESMod can only handel one heating set back.") 

425 return t_set_zone_nominal, start_time, width, amplitude 

426 

427 

428def _get_next_higher_year_value(years_dict, given_year): 

429 """ 

430 Get the next higher value for a given year from a dictionary. 

431 

432 Parameters 

433 ---------- 

434 years_dict : dict 

435 Dictionary with years as keys and corresponding values. 

436 given_year : int 

437 Year to find the next higher value for. 

438 

439 Returns 

440 ------- 

441 float or int 

442 Value corresponding to the next higher year. If no higher year is found, 

443 returns the value of the latest year. 

444 """ 

445 years = sorted(years_dict.keys()) 

446 for year in years: 

447 if year > given_year: 

448 return years_dict[year] 

449 return years_dict[years[-1]] 

450 

451 

452def _help_example_script(bldg, dir_dymola, test_script_template, example): 

453 """ 

454 Create a .mos script for simulating and plotting BESMod examples from a Mako template. 

455 

456 Parameters 

457 ---------- 

458 bldg : Building 

459 TEASER Building instance for which the script is created. 

460 dir_dymola : str 

461 Output directory for Dymola scripts. 

462 test_script_template : Template 

463 Mako template for the simulation script. 

464 example : str 

465 Name of the BESMod example. 

466 """ 

467 

468 dir_building = utilities.create_path(os.path.join(dir_dymola, bldg.name)) 

469 with open(os.path.join(dir_building, example + bldg.name + ".mos"), 'w') as out_file: 

470 out_file.write(test_script_template.render_unicode( 

471 project=bldg.parent, 

472 bldg=bldg 

473 )) 

474 out_file.close()