"""This module contains function for BESMod model generation"""
import os
from typing import Optional, Union, List, Dict
from mako.template import Template
from mako.lookup import TemplateLookup
import teaser.logic.utilities as utilities
import teaser.data.output.modelica_output as modelica_output
from teaser.logic.buildingobjects.building import Building
[docs]
def export_besmod(
buildings: Union[List[Building], Building],
prj: 'Project',
path: Optional[str] = None,
examples: Optional[List[str]] = None,
THydSup_nominal: Optional[Union[float, Dict[str, float]]] = None,
QBuiOld_flow_design: Optional[Dict[str, Dict[str, float]]] = None,
THydSupOld_design: Optional[Union[float, Dict[str, float]]] = None,
custom_examples: Optional[Dict[str, str]] = None,
custom_script: Optional[Dict[str, str]] = None
) -> None:
"""
Export building models for BESMod simulations.
This function generates BESMod.Systems.Demand.Building.TEASERThermalZone models
for one or more TEASER buildings. It also allows exporting examples from
BESMod.Examples, including the building models.
Parameters
----------
buildings : Union[List[Building], Building]
TEASER Building instances to export as BESMod models. Can be a single
Building or a list of Buildings.
prj : Project
TEASER Project instance containing project metadata such as library
versions and weather file paths.
examples : Optional[List[str]]
Names of BESMod examples to export alongside the building models.
Supported Examples: "TEASERHeatLoadCalculation", "HeatPumpMonoenergetic", and "GasBoilerBuildingOnly".
path : Optional[str]
Alternative output path for storing the exported files. If None, the default TEASER output path is used.
THydSup_nominal : Optional[Union[float, Dict[str, float]]]
Nominal supply temperature(s) for the hydraulic system. Required for
certain examples (e.g., HeatPumpMonoenergetic, GasBoilerBuildingOnly).
See docstring of teaser.data.output.besmod_output.convert_input() for further information.
QBuiOld_flow_design : Optional[Dict[str, Dict[str, float]]]
For partially retrofitted systems specify the old nominal heat flow
of all zones in the Buildings in a nested dictionary with
the building names and in a level below the zone names as keys.
By default, only the radiator transfer system is not retrofitted in BESMod.
THydSupOld_design : Optional[Union[float, Dict[str, float]]]
Design supply temperatures for old, non-retrofitted hydraulic systems.
custom_examples: Optional[Dict[str, str]]
Specify custom examples with a dictionary containing the example name as the key and
the path to the corresponding custom mako template as the value.
custom_script: Optional[Dict[str, str]]
Specify custom .mos scripts for the existing and custom examples with a dictionary
containing the example name as the key and the path to the corresponding custom mako template as the value.
Raises
------
ValueError
If given example is not supported.
ValueError
If `THydSup_nominal` is not provided for examples requiring it.
AssertionError
If the used library for calculations is not AixLib.
NotImplementedError
If a building uses a thermal zone model other than the four-element model.
Notes
-----
The function uses Mako templates for generating Modelica models.
"""
if prj.used_library_calc != "AixLib":
raise AttributeError("BESMod export is only implemented for AixLib calculation.")
if examples is None:
examples = []
if not isinstance(examples, list):
examples = [examples]
supported_examples = [
"TEASERHeatLoadCalculation",
"HeatPumpMonoenergetic",
"GasBoilerBuildingOnly",
]
for exp in examples:
if exp not in supported_examples:
raise ValueError(
f"Example {exp} is not supported. "
f"Supported examples are {supported_examples}."
)
if THydSup_nominal is None and any(
example in examples for example in ["HeatPumpMonoenergetic", "GasBoilerBuildingOnly"]
):
raise ValueError(
"Examples 'HeatPumpMonoenergetic' and 'GasBoilerBuildingOnly' "
"require the `THydSup_nominal` parameter."
)
t_hyd_sup_nominal_bldg = convert_input(THydSup_nominal, buildings)
t_hyd_sup_old_design_bldg = (
convert_input(THydSupOld_design, buildings)
if THydSupOld_design
else {bldg.name: "systemParameters.THydSup_nominal" for bldg in buildings}
)
if QBuiOld_flow_design is None:
QBuiOld_flow_design = {
bldg.name: "systemParameters.QBui_flow_nominal" for bldg in buildings
}
else:
QBuiOld_flow_design = {
bldg.name: _convert_to_zone_array(bldg, QBuiOld_flow_design[bldg.name])
for bldg in buildings
}
if custom_script is None:
custom_script = {}
dir_resources = utilities.create_path(os.path.join(path, "Resources"))
dir_scripts = utilities.create_path(os.path.join(dir_resources, "Scripts"))
dir_dymola = utilities.create_path(os.path.join(dir_scripts, "Dymola"))
template_path = utilities.get_full_path("data/output/modelicatemplate")
lookup = TemplateLookup(directories=[template_path])
zone_template_4 = Template(
filename=os.path.join(template_path, "AixLib/AixLib_ThermalZoneRecord_FourElement"),
lookup=lookup)
building_template = Template(
filename=os.path.join(template_path, "BESMod/Building"),
lookup=lookup)
uses = [
'Modelica(version="' + prj.modelica_info.version + '")',
'AixLib(version="' + prj.buildings[-1].library_attr.version + '")',
'BESMod(version="' + prj.buildings[-1].library_attr.besmod_version + '")']
modelica_output.create_package(
path=path,
name=prj.name,
uses=uses)
modelica_output.create_package_order(
path=path,
package_list=buildings)
modelica_output.copy_weather_data(prj.weather_file_path, dir_resources)
for i, bldg in enumerate(buildings):
bldg.bldg_height = bldg.number_of_floors * bldg.height_of_floors
start_time_zones = []
width_zones = []
amplitude_zones = []
t_set_zone_nominal = []
for tz in bldg.thermal_zones:
heating_profile = tz.use_conditions.heating_profile
t_set_nominal, start_time, width, amplitude = _convert_heating_profile(heating_profile)
t_set_zone_nominal.append(t_set_nominal)
amplitude_zones.append(amplitude)
start_time_zones.append(start_time)
width_zones.append(width)
bldg_path = os.path.join(path, bldg.name)
utilities.create_path(bldg_path)
utilities.create_path(os.path.join(bldg_path, bldg.name + "_DataBase"))
bldg.library_attr.modelica_gains_boundary(path=bldg_path)
with open(utilities.get_full_path(
os.path.join(bldg_path, bldg.name + ".mo")), 'w') as out_file:
out_file.write(building_template.render_unicode(
bldg=bldg))
out_file.close()
def write_example_mo(example_template, example):
with open(utilities.get_full_path(
os.path.join(bldg_path, example + bldg.name + ".mo")), 'w') as model_file:
model_file.write(example_template.render_unicode(
bldg=bldg,
project=prj,
TOda_nominal=bldg.thermal_zones[0].t_outside,
THydSup_nominal=t_hyd_sup_nominal_bldg[bldg.name],
TSetZone_nominal=t_set_zone_nominal,
QBuiOld_flow_design=QBuiOld_flow_design[bldg.name],
THydSupOld_design=t_hyd_sup_old_design_bldg[bldg.name],
setBakTSetZone_amplitude=amplitude_zones,
setBakTSetZone_startTime=start_time_zones,
setBakTSetZone_width=width_zones))
model_file.close()
for exp in examples:
exp_template = Template(
filename=utilities.get_full_path(
"data/output/modelicatemplate/BESMod/Example_" + exp),
lookup=lookup)
if exp in custom_script.keys():
example_sim_plot_script = Template(
filename=custom_script[exp],
lookup=lookup)
else:
example_sim_plot_script = Template(
filename=utilities.get_full_path(
"data/output/modelicatemplate/BESMod/Script_" + exp),
lookup=lookup)
_help_example_script(bldg, dir_dymola, example_sim_plot_script, exp)
write_example_mo(exp_template, exp)
bldg_package = [exp + bldg.name for exp in examples]
if custom_examples:
for exp, c_path in custom_examples.items():
bldg_package.append(exp + bldg.name)
exp_template = Template(
filename=c_path,
lookup=lookup)
write_example_mo(exp_template, exp)
if exp in custom_script.keys():
example_sim_plot_script = Template(
filename=custom_script[exp],
lookup=lookup)
_help_example_script(bldg, dir_dymola, example_sim_plot_script, exp)
bldg_package.append(bldg.name + "_DataBase")
modelica_output.create_package(path=bldg_path, name=bldg.name, within=bldg.parent.name)
modelica_output.create_package_order(
path=bldg_path,
package_list=[bldg],
extra=bldg_package)
zone_path = os.path.join(bldg_path, bldg.name + "_DataBase")
for zone in bldg.thermal_zones:
zone.use_conditions.with_heating = False
with open(utilities.get_full_path(os.path.join(
zone_path,
bldg.name + '_' + zone.name + '.mo')), 'w') as out_file:
if type(zone.model_attr).__name__ == "FourElement":
out_file.write(zone_template_4.render_unicode(zone=zone))
else:
raise NotImplementedError("BESMod export is only implemented for four elements.")
out_file.close()
modelica_output.create_package(
path=zone_path,
name=bldg.name + '_DataBase',
within=prj.name + '.' + bldg.name)
modelica_output.create_package_order(
path=zone_path,
package_list=bldg.thermal_zones,
addition=bldg.name + "_")
print("Exports can be found here:")
print(path)
def _convert_to_zone_array(bldg, zone_dict):
"""
Convert a dictionary of zone values to a BESMod-compatible array string.
Parameters
----------
bldg : Building
TEASER Building instance.
zone_dict : dict
Dictionary with zone names as keys and zone parameter values as values.
Returns
-------
str
Array string for BESMod parameter input.
Raises
------
KeyError
If the dictionary is missing zone names present in the building.
"""
tz_names = [tz.name for tz in bldg.thermal_zones]
if set(tz_names) == set(list(zone_dict.keys())):
array_string = "{"
for tz in tz_names:
array_string += str(zone_dict[tz]) + ","
return array_string[:-1] + "}"
else:
raise KeyError(f"{set(tz_names) - set(list(zone_dict.keys()))} thermal zones missing in given dictionary.")
def _convert_heating_profile(heating_profile):
"""
Convert a 24-hour heating profile for BESMod export.
This function analyzes a 24-hour heating profile to extract:
- The nominal temperature.
- Start time of setbacks (if any).
- Width of setback intervals.
- Amplitude of the heating variation.
Parameters
----------
heating_profile : list[float]
List of 24 hourly heating temperatures.
Returns
-------
t_set_zone_nominal : float
Maximum temperature in the profile, used as the nominal set point.
start_time : int
Start time of the setback interval in seconds.
width : float
Width of the setback interval as a percentage of the day.
amplitude : float
Difference between the minimum and nominal temperatures.
Raises
------
ValueError
If the profile has more than two distinct intervals or does not have 24 values.
"""
if len(heating_profile) != 24:
raise ValueError("Only 24 hours heating profiles can be used for BESMod export.")
change_count = 0
change_indexes = []
for i in range(1, len(heating_profile)):
if heating_profile[i] != heating_profile[i - 1]:
change_count += 1
change_indexes.append(i)
t_set_zone_nominal = max(heating_profile)
amplitude = min(heating_profile) - t_set_zone_nominal
if change_count == 0:
amplitude = 0
start_time = 0
width = 1e-50
elif change_count == 1:
if heating_profile[0] < heating_profile[-1]:
start_time = 0
width = 100 * change_indexes[0] / 24
else:
start_time = change_indexes[0] * 3600
width = 100 * (24 - change_indexes[0]) / 24
elif change_count == 2:
start_time = change_indexes[1] * 3600
width = 100 * (24 - change_indexes[1] + change_indexes[0]) / 24
else:
raise ValueError("You have more than two temperature intervals in the heating profile."
"BESMod can only handel one heating set back.")
return t_set_zone_nominal, start_time, width, amplitude
def _get_next_higher_year_value(years_dict, given_year):
"""
Get the next higher value for a given year from a dictionary.
Parameters
----------
years_dict : dict
Dictionary with years as keys and corresponding values.
given_year : int
Year to find the next higher value for.
Returns
-------
float or int
Value corresponding to the next higher year. If no higher year is found,
returns the value of the latest year.
"""
years = sorted(years_dict.keys())
for year in years:
if year > given_year:
return years_dict[year]
return years_dict[years[-1]]
def _help_example_script(bldg, dir_dymola, test_script_template, example):
"""
Create a .mos script for simulating and plotting BESMod examples from a Mako template.
Parameters
----------
bldg : Building
TEASER Building instance for which the script is created.
dir_dymola : str
Output directory for Dymola scripts.
test_script_template : Template
Mako template for the simulation script.
example : str
Name of the BESMod example.
"""
dir_building = utilities.create_path(os.path.join(dir_dymola, bldg.name))
with open(os.path.join(dir_building, example + bldg.name + ".mos"), 'w') as out_file:
out_file.write(test_script_template.render_unicode(
project=bldg.parent,
bldg=bldg
))
out_file.close()