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
« 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"""
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
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.
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.
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.
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.
73 Notes
74 -----
75 The function uses Mako templates for generating Modelica models.
76 """
78 if prj.used_library_calc != "AixLib":
79 raise AttributeError("BESMod export is only implemented for AixLib calculation.")
81 if examples is None:
82 examples = []
84 if path is None:
85 path = utilities.get_full_path("")
87 if not isinstance(examples, list):
88 examples = [examples]
90 supported_examples = [
91 "TEASERHeatLoadCalculation",
92 "HeatPumpMonoenergetic",
93 "GasBoilerBuildingOnly",
94 ]
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 )
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.")
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 )
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 }
134 if custom_script is None:
135 custom_script = {}
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])
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)
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)
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)
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)
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()
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()
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)
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)
240 zone_path = os.path.join(bldg_path, bldg.name + "_DataBase")
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()
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 + "_")
262 print("Exports can be found here:")
263 print(path)
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.
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.
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.
297 Returns
298 -------
299 Dict[str, str]
300 Dictionary mapping building names to BESMod parameter input strings.
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
335def _convert_to_zone_array(bldg, zone_dict):
336 """
337 Convert a dictionary of zone values to a BESMod-compatible array string.
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.
346 Returns
347 -------
348 str
349 Array string for BESMod parameter input.
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.")
366def _convert_heating_profile(heating_profile):
367 """
368 Convert a 24-hour heating profile for BESMod export.
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.
376 Parameters
377 ----------
378 heating_profile : list[float]
379 List of 24 hourly heating temperatures.
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.
392 Raises
393 ------
394 ValueError
395 If the profile has more than two distinct intervals or does not have 24 values.
396 """
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
428def _get_next_higher_year_value(years_dict, given_year):
429 """
430 Get the next higher value for a given year from a dictionary.
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.
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]]
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.
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 """
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()