Merge branch 'energy_plus_export' into 'master'

IDF export improvement.

See merge request Guille/hub!29
This commit is contained in:
Guillermo Gutierrez Morote 2022-11-07 20:05:52 +00:00
commit 3b9d2e4de9
4 changed files with 9886 additions and 61 deletions

View File

@ -17,13 +17,14 @@ class ExportsFactory:
""" """
Exports factory class Exports factory class
""" """
def __init__(self, export_type, city, path, target_buildings=None): def __init__(self, export_type, city, path, target_buildings=None, adjacent_buildings=None):
self._city = city self._city = city
self._export_type = '_' + export_type.lower() self._export_type = '_' + export_type.lower()
if isinstance(path, str): if isinstance(path, str):
path = Path(path) path = Path(path)
self._path = path self._path = path
self._target_buildings = target_buildings self._target_buildings = target_buildings
self._adjacent_buildings = adjacent_buildings
@property @property
def _citygml(self): def _citygml(self):
@ -73,12 +74,20 @@ class ExportsFactory:
def _idf(self): def _idf(self):
""" """
Export the city to Energy+ idf format Export the city to Energy+ idf format
When target_buildings is set, only those will be calculated and their energy consumption output, non adjacent
buildings will be considered shading objects and adjacent buildings will be considered adiabatic.
Adjacent buildings are provided they will be considered heated so energy plus calculations are more precise but
no results will be calculated to speed up the calculation process.
:return: None :return: None
""" """
idf_data_path = (Path(__file__).parent / './formats/idf_files/').resolve() idf_data_path = (Path(__file__).parent / './formats/idf_files/').resolve()
# todo: create a get epw file function based on the city # todo: create a get epw file function based on the city
weather_path = (Path(__file__).parent / '../data/weather/epw/CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve() weather_path = (Path(__file__).parent / '../data/weather/epw/CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve()
return Idf(self._city, self._path, (idf_data_path / 'Minimal.idf'), (idf_data_path / 'Energy+.idd'), weather_path) return Idf(self._city, self._path, (idf_data_path / 'Minimal.idf'), (idf_data_path / 'Energy+.idd'), weather_path,
target_buildings=self._target_buildings, adjacent_buildings=self._adjacent_buildings)
@property @property
def _sra(self): def _sra(self):

View File

@ -18,35 +18,40 @@ class Idf:
""" """
Exports city to IDF Exports city to IDF
""" """
_THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT' _BUILDING = 'BUILDING'
_IDEAL_LOAD_AIR_SYSTEM = 'HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM'
_SURFACE = 'BUILDINGSURFACE:DETAILED'
_CONSTRUCTION = 'CONSTRUCTION'
_MATERIAL = 'MATERIAL'
_MATERIAL_NOMASS = 'MATERIAL:NOMASS'
_ROUGHNESS = 'MediumRough'
_HOURLY_SCHEDULE = 'SCHEDULE:DAY:HOURLY'
_COMPACT_SCHEDULE = 'SCHEDULE:COMPACT'
_FILE_SCHEDULE = 'SCHEDULE:FILE'
_ZONE = 'ZONE' _ZONE = 'ZONE'
_LIGHTS = 'LIGHTS' _LIGHTS = 'LIGHTS'
_PEOPLE = 'PEOPLE' _PEOPLE = 'PEOPLE'
_INFILTRATION = 'ZONEINFILTRATION:DESIGNFLOWRATE' _THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT'
_IDEAL_LOAD_AIR_SYSTEM = 'HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM'
_SURFACE = 'BUILDINGSURFACE:DETAILED'
_SHADING = 'SHADING:BUILDING:DETAILED'
_SHADING_PROPERTY = 'SHADINGPROPERTY:REFLECTANCE'
_BUILDING_SURFACE = 'BuildingSurfaceDetailed' _BUILDING_SURFACE = 'BuildingSurfaceDetailed'
_CONSTRUCTION = 'CONSTRUCTION'
_MATERIAL = 'MATERIAL'
_MATERIAL_NOMASS = 'MATERIAL:NOMASS'
_MATERIAL_ROOFVEGETATION = 'MATERIAL:ROOFVEGETATION'
_WINDOW = 'WINDOW'
_WINDOW_MATERIAL_SIMPLE = 'WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM'
_ROUGHNESS = 'MediumRough'
_INFILTRATION = 'ZONEINFILTRATION:DESIGNFLOWRATE'
_HOURLY_SCHEDULE = 'SCHEDULE:DAY:HOURLY'
_COMPACT_SCHEDULE = 'SCHEDULE:COMPACT'
_FILE_SCHEDULE = 'SCHEDULE:FILE'
_SCHEDULE_LIMIT = 'SCHEDULETYPELIMITS' _SCHEDULE_LIMIT = 'SCHEDULETYPELIMITS'
_ON_OFF = 'On/Off' _ON_OFF = 'On/Off'
_FRACTION = 'Fraction' _FRACTION = 'Fraction'
_ANY_NUMBER = 'Any Number' _ANY_NUMBER = 'Any Number'
_CONTINUOUS = 'Continuous' _CONTINUOUS = 'Continuous'
_DISCRETE = 'Discrete' _DISCRETE = 'Discrete'
_BUILDING = 'BUILDING'
_SIZING_PERIODS = 'SIZINGPERIOD:DESIGNDAY' _SIZING_PERIODS = 'SIZINGPERIOD:DESIGNDAY'
_LOCATION = 'SITE:LOCATION' _LOCATION = 'SITE:LOCATION'
_WINDOW_MATERIAL_SIMPLE = 'WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM'
_WINDOW = 'WINDOW'
_MATERIAL_ROOFVEGETATION = 'MATERIAL:ROOFVEGETATION'
_SIMPLE = 'Simple' _SIMPLE = 'Simple'
idf_surfaces = { idf_surfaces = {
# todo: make an enum for all the surface types # todo: make an enum for all the surface types
cte.WALL: 'wall', cte.WALL: 'wall',
@ -91,7 +96,8 @@ class Idf:
'list': 'List', 'list': 'List',
} }
def __init__(self, city, output_path, idf_file_path, idd_file_path, epw_file_path, export_type="Surfaces"): def __init__(self, city, output_path, idf_file_path, idd_file_path, epw_file_path, export_type="Surfaces",
target_buildings=None, adjacent_buildings=None):
self._city = city self._city = city
self._output_path = str(output_path.resolve()) self._output_path = str(output_path.resolve())
self._output_file = str((output_path / f'{city.name}.idf').resolve()) self._output_file = str((output_path / f'{city.name}.idf').resolve())
@ -106,6 +112,12 @@ class Idf:
Numeric_Type=self._CONTINUOUS) Numeric_Type=self._CONTINUOUS)
self._idf.newidfobject(self._SCHEDULE_LIMIT, Name=self._ON_OFF, Lower_Limit_Value=0, Upper_Limit_Value=1, self._idf.newidfobject(self._SCHEDULE_LIMIT, Name=self._ON_OFF, Lower_Limit_Value=0, Upper_Limit_Value=1,
Numeric_Type=self._DISCRETE) Numeric_Type=self._DISCRETE)
self._target_buildings = target_buildings
if target_buildings is None:
self._target_buildings = [building.name for building in self._city.buildings]
self._adjacent_buildings = adjacent_buildings
if self._adjacent_buildings is None:
self._adjacent_buildings = []
self._export() self._export()
@staticmethod @staticmethod
@ -285,7 +297,7 @@ class Idf:
def _add_window_construction_and_material(self, thermal_opening): def _add_window_construction_and_material(self, thermal_opening):
for window_material in self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]: for window_material in self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]:
if window_material['UFactor'] == thermal_opening.overall_u_value and \ if window_material['UFactor'] == thermal_opening.overall_u_value and \
window_material['Solar_Heat_Gain_Coefficient'] == thermal_opening.g_value: window_material['Solar_Heat_Gain_Coefficient'] == thermal_opening.g_value:
return return
order = str(len(self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]) + 1) order = str(len(self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]) + 1)
@ -298,13 +310,13 @@ class Idf:
_kwargs = {'Name': window_construction_name, 'Outside_Layer': material_name} _kwargs = {'Name': window_construction_name, 'Outside_Layer': material_name}
self._idf.newidfobject(self._CONSTRUCTION, **_kwargs) self._idf.newidfobject(self._CONSTRUCTION, **_kwargs)
def _add_zone(self, thermal_zone): def _add_zone(self, thermal_zone, name):
for zone in self._idf.idfobjects['ZONE']: for zone in self._idf.idfobjects['ZONE']:
if zone.Name == thermal_zone.id: if zone.Name == name:
return return
# todo: what do we need to define a zone in energy plus? # todo: what do we need to define a zone in energy plus?
self._idf.newidfobject(self._ZONE, Name=thermal_zone.id, Volume=thermal_zone.volume) self._idf.newidfobject(self._ZONE, Name=name, Volume=thermal_zone.volume)
self._add_heating_system(thermal_zone) self._add_heating_system(thermal_zone, name)
def _add_thermostat(self, thermal_zone): def _add_thermostat(self, thermal_zone):
thermostat_name = f'Thermostat {thermal_zone.usage}' thermostat_name = f'Thermostat {thermal_zone.usage}'
@ -317,26 +329,26 @@ class Idf:
Heating_Setpoint_Schedule_Name=f'Heating thermostat schedules {thermal_zone.usage}', Heating_Setpoint_Schedule_Name=f'Heating thermostat schedules {thermal_zone.usage}',
Cooling_Setpoint_Schedule_Name=f'Cooling thermostat schedules {thermal_zone.usage}') Cooling_Setpoint_Schedule_Name=f'Cooling thermostat schedules {thermal_zone.usage}')
def _add_heating_system(self, thermal_zone): def _add_heating_system(self, thermal_zone, zone_name):
for air_system in self._idf.idfobjects[self._IDEAL_LOAD_AIR_SYSTEM]: for air_system in self._idf.idfobjects[self._IDEAL_LOAD_AIR_SYSTEM]:
if air_system.Zone_Name == thermal_zone.id: if air_system.Zone_Name == zone_name:
return return
thermostat = self._add_thermostat(thermal_zone) thermostat = self._add_thermostat(thermal_zone)
self._idf.newidfobject(self._IDEAL_LOAD_AIR_SYSTEM, self._idf.newidfobject(self._IDEAL_LOAD_AIR_SYSTEM,
Zone_Name=thermal_zone.id, Zone_Name=zone_name,
System_Availability_Schedule_Name=f'HVAC AVAIL SCHEDULES {thermal_zone.usage}', System_Availability_Schedule_Name=f'HVAC AVAIL SCHEDULES {thermal_zone.usage}',
Heating_Availability_Schedule_Name=f'HVAC AVAIL SCHEDULES {thermal_zone.usage}', Heating_Availability_Schedule_Name=f'HVAC AVAIL SCHEDULES {thermal_zone.usage}',
Cooling_Availability_Schedule_Name=f'HVAC AVAIL SCHEDULES {thermal_zone.usage}', Cooling_Availability_Schedule_Name=f'HVAC AVAIL SCHEDULES {thermal_zone.usage}',
Template_Thermostat_Name=thermostat.Name) Template_Thermostat_Name=thermostat.Name)
def _add_occupancy(self, thermal_zone): def _add_occupancy(self, thermal_zone, zone_name):
number_of_people = thermal_zone.occupancy.occupancy_density * thermal_zone.total_floor_area number_of_people = thermal_zone.occupancy.occupancy_density * thermal_zone.total_floor_area
fraction_radiant = thermal_zone.occupancy.sensible_radiative_internal_gain / \ fraction_radiant = thermal_zone.occupancy.sensible_radiative_internal_gain / \
(thermal_zone.occupancy.sensible_radiative_internal_gain + (thermal_zone.occupancy.sensible_radiative_internal_gain +
thermal_zone.occupancy.sensible_convective_internal_gain) thermal_zone.occupancy.sensible_convective_internal_gain)
self._idf.newidfobject(self._PEOPLE, self._idf.newidfobject(self._PEOPLE,
Name=f'{thermal_zone.id}_occupancy', Name=f'{zone_name}_occupancy',
Zone_or_ZoneList_Name=thermal_zone.id, Zone_or_ZoneList_Name=zone_name,
Number_of_People_Schedule_Name=f'Occupancy schedules {thermal_zone.usage}', Number_of_People_Schedule_Name=f'Occupancy schedules {thermal_zone.usage}',
Number_of_People_Calculation_Method="People", Number_of_People_Calculation_Method="People",
Number_of_People=number_of_people, Number_of_People=number_of_people,
@ -344,21 +356,22 @@ class Idf:
Activity_Level_Schedule_Name=f'Activity Level schedules {thermal_zone.usage}' Activity_Level_Schedule_Name=f'Activity Level schedules {thermal_zone.usage}'
) )
def _add_infiltration(self, thermal_zone): def _add_infiltration(self, thermal_zone, zone_name):
for zone in self._idf.idfobjects["ZONE"]: for zone in self._idf.idfobjects["ZONE"]:
if zone.Name == f'{thermal_zone.id}_infiltration': if zone.Name == f'{zone_name}_infiltration':
return return
self._idf.newidfobject(self._INFILTRATION, self._idf.newidfobject(self._INFILTRATION,
Name=f'{thermal_zone.id}_infiltration', Name=f'{zone_name}_infiltration',
Zone_or_ZoneList_Name=thermal_zone.id, Zone_or_ZoneList_Name=zone_name,
Schedule_Name=f'Infiltration schedules {thermal_zone.usage}', Schedule_Name=f'Infiltration schedules {thermal_zone.usage}',
Design_Flow_Rate_Calculation_Method='AirChanges/Hour', Design_Flow_Rate_Calculation_Method='AirChanges/Hour',
Air_Changes_per_Hour=thermal_zone.mechanical_air_change Air_Changes_per_Hour=thermal_zone.mechanical_air_change
) )
def _rename_building(self, b): def _rename_building(self, city_name):
name = str(str(city_name.encode("utf-8")))
for building in self._idf.idfobjects[self._BUILDING]: for building in self._idf.idfobjects[self._BUILDING]:
building.Name = b.name building.Name = f'Buildings in {name}'
building['Solar_Distribution'] = 'FullExterior' building['Solar_Distribution'] = 'FullExterior'
def _remove_sizing_periods(self): def _remove_sizing_periods(self):
@ -370,14 +383,20 @@ class Idf:
def _export(self): def _export(self):
""" """
Export the idf file into the given path Export the idf file into the given path.
If buildings to calculate are provided, only those will appear in the output variables, otherwise all the city
buildings will be calculated.
If adjacent buildings are provided those buildings will be calculated, but will not appear in the output variables.
export type = "Surfaces|Block" export type = "Surfaces|Block"
""" """
self._remove_location() self._remove_location()
self._remove_sizing_periods() self._remove_sizing_periods()
self._rename_building(self._city.name)
for building in self._city.buildings: for building in self._city.buildings:
self._rename_building(building)
for internal_zone in building.internal_zones: for internal_zone in building.internal_zones:
for thermal_zone in internal_zone.thermal_zones: for thermal_zone in internal_zone.thermal_zones:
for thermal_boundary in thermal_zone.thermal_boundaries: for thermal_boundary in thermal_zone.thermal_boundaries:
@ -387,31 +406,40 @@ class Idf:
for thermal_opening in thermal_boundary.thermal_openings: for thermal_opening in thermal_boundary.thermal_openings:
self._add_window_construction_and_material(thermal_opening) self._add_window_construction_and_material(thermal_opening)
usage = thermal_zone.usage usage = thermal_zone.usage
self._add_infiltration_schedules(thermal_zone) if building.name in self._target_buildings or building.name in self._adjacent_buildings:
self._add_schedules(usage, 'Occupancy', thermal_zone.occupancy.occupancy_schedules) self._add_infiltration_schedules(thermal_zone)
self._add_schedules(usage, 'HVAC AVAIL', thermal_zone.thermal_control.hvac_availability_schedules) self._add_schedules(usage, 'Occupancy', thermal_zone.occupancy.occupancy_schedules)
self._add_schedules(usage, 'Heating thermostat', thermal_zone.thermal_control.heating_set_point_schedules) self._add_schedules(usage, 'HVAC AVAIL', thermal_zone.thermal_control.hvac_availability_schedules)
self._add_schedules(usage, 'Cooling thermostat', thermal_zone.thermal_control.cooling_set_point_schedules) self._add_schedules(usage, 'Heating thermostat', thermal_zone.thermal_control.heating_set_point_schedules)
self._add_people_activity_level_schedules(thermal_zone) self._add_schedules(usage, 'Cooling thermostat', thermal_zone.thermal_control.cooling_set_point_schedules)
self._add_people_activity_level_schedules(thermal_zone)
self._add_zone(thermal_zone) self._add_zone(thermal_zone, building.name)
self._add_heating_system(thermal_zone) self._add_heating_system(thermal_zone, building.name)
self._add_infiltration(thermal_zone) self._add_infiltration(thermal_zone, building.name)
self._add_occupancy(thermal_zone) self._add_occupancy(thermal_zone, building.name)
if self._export_type == "Surfaces": if self._export_type == "Surfaces":
self._add_surfaces(building) if building.name in self._target_buildings or building.name in self._adjacent_buildings:
self._add_surfaces(building, building.name)
else:
self._add_shading(building)
else: else:
self._add_block(building) self._add_block(building)
# TODO: this should change to specific variables per zone to process only the ones in the buildings_to_calculate
for building in self._target_buildings:
continue
self._idf.newidfobject( self._idf.newidfobject(
"OUTPUT:VARIABLE", "OUTPUT:VARIABLE",
Variable_Name="Zone Ideal Loads Supply Air Total Heating Energy", Variable_Name="Zone Ideal Loads Supply Air Total Heating Energy",
Reporting_Frequency="Hourly", Reporting_Frequency="Hourly",
) )
self._idf.newidfobject( self._idf.newidfobject(
"OUTPUT:VARIABLE", "OUTPUT:VARIABLE",
Variable_Name="Zone Ideal Loads Supply Air Total Cooling Energy", Variable_Name="Zone Ideal Loads Supply Air Total Cooling Energy",
Reporting_Frequency="Hourly", Reporting_Frequency="Hourly",
) )
self._idf.match() self._idf.match()
try: try:
self._idf.intersect_match() self._idf.intersect_match()
@ -445,7 +473,25 @@ class Idf:
break break
self._idf.intersect_match() self._idf.intersect_match()
def _add_surfaces(self, building): def _add_shading(self, building):
for internal_zone in building.internal_zones:
for thermal_zone in internal_zone.thermal_zones:
for boundary in thermal_zone.thermal_boundaries:
shading = self._idf.newidfobject(self._SHADING, Name=f'{boundary.parent_surface.name}')
coordinates = self._matrix_to_list(boundary.parent_surface.solid_polygon.coordinates,
self._city.lower_corner)
shading.setcoords(coordinates)
solar_reflectance = 1.0 - boundary.outside_solar_absorptance
visible_reflectance = 1.0 - boundary.outside_visible_absorptance
self._idf.newidfobject(self._SHADING_PROPERTY,
Shading_Surface_Name=f'{boundary.parent_surface.name}',
Diffuse_Solar_Reflectance_of_Unglazed_Part_of_Shading_Surface=solar_reflectance,
Diffuse_Visible_Reflectance_of_Unglazed_Part_of_Shading_Surface=visible_reflectance,
Fraction_of_Shading_Surface_That_Is_Glazed=0)
# TODO: Add properties for that name
def _add_surfaces(self, building, zone_name):
for internal_zone in building.internal_zones: for internal_zone in building.internal_zones:
for thermal_zone in internal_zone.thermal_zones: for thermal_zone in internal_zone.thermal_zones:
for boundary in thermal_zone.thermal_boundaries: for boundary in thermal_zone.thermal_boundaries:
@ -463,7 +509,7 @@ class Idf:
construction_name = boundary.construction_name construction_name = boundary.construction_name
surface = self._idf.newidfobject(self._SURFACE, Name=f'{boundary.parent_surface.name}', surface = self._idf.newidfobject(self._SURFACE, Name=f'{boundary.parent_surface.name}',
Surface_Type=idf_surface_type, Surface_Type=idf_surface_type,
Zone_Name=thermal_zone.id, Zone_Name=zone_name,
Construction_Name=construction_name, Construction_Name=construction_name,
Outside_Boundary_Condition=outside_boundary_condition, Outside_Boundary_Condition=outside_boundary_condition,
Sun_Exposure=sun_exposure, Sun_Exposure=sun_exposure,
@ -491,7 +537,7 @@ class Idf:
for material in self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]: for material in self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]:
if material['Name'] == glazing: if material['Name'] == glazing:
if material['UFactor'] == opening.overall_u_value and \ if material['UFactor'] == opening.overall_u_value and \
material['Solar_Heat_Gain_Coefficient'] == opening.g_value: material['Solar_Heat_Gain_Coefficient'] == opening.g_value:
return True return True
return False return False
@ -533,5 +579,4 @@ class Idf:
soil.residual_volumetric_moisture_content, soil.residual_volumetric_moisture_content,
Initial_Volumetric_Moisture_Content_of_the_Soil_Layer= Initial_Volumetric_Moisture_Content_of_the_Soil_Layer=
soil.initial_volumetric_moisture_content, soil.initial_volumetric_moisture_content,
Moisture_Diffusion_Calculation_Method=self._SIMPLE Moisture_Diffusion_Calculation_Method=self._SIMPLE)
)

View File

@ -89,3 +89,25 @@ class TestExports(TestCase):
export to SRA export to SRA
""" """
self._export('sra') self._export('sra')
def test_idf_export(self):
"""
export to IDF
"""
city = self._get_citygml('EV_GM_MB_LoD2.gml')
for building in city.buildings:
building.year_of_construction = 2006
if building.function is None:
building.function = 'large office'
ConstructionFactory('nrel', city).enrich()
UsageFactory('comnet', city).enrich()
try:
ExportsFactory('idf', city, self._output_path).export()
ExportsFactory('idf', city, self._output_path, target_buildings=['gml_1066158', 'gml_1']).export()
ExportsFactory('idf', city, self._output_path, target_buildings=['gml_1066158'],
adjacent_buildings=['gml_1']).export()
ExportsFactory('idf', city, self._output_path, target_buildings=['gml_1066158']).export()
except Exception:
self.fail("Idf ExportsFactory raised ExceptionType unexpectedly!")

File diff suppressed because it is too large Load Diff