IDF export improvement.

IDF export now allows the user to select the target and adjacent buildings
 - Only target and adjacent buildings in the city are calculated.
 - All other buildings are considered shade objects.

The zones are properly labeled with the building name
The "Building" is labeled as a city
This commit is contained in:
Guille Gutierrez 2022-11-07 15:04:51 -05:00
parent 41c66106fa
commit 448845a234
4 changed files with 9886 additions and 61 deletions

View File

@ -17,13 +17,14 @@ class ExportsFactory:
"""
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._export_type = '_' + export_type.lower()
if isinstance(path, str):
path = Path(path)
self._path = path
self._target_buildings = target_buildings
self._adjacent_buildings = adjacent_buildings
@property
def _citygml(self):
@ -73,12 +74,20 @@ class ExportsFactory:
def _idf(self):
"""
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
"""
idf_data_path = (Path(__file__).parent / './formats/idf_files/').resolve()
# 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()
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
def _sra(self):

View File

@ -18,35 +18,40 @@ class Idf:
"""
Exports city to IDF
"""
_THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT'
_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'
_BUILDING = 'BUILDING'
_ZONE = 'ZONE'
_LIGHTS = 'LIGHTS'
_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'
_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'
_ON_OFF = 'On/Off'
_FRACTION = 'Fraction'
_ANY_NUMBER = 'Any Number'
_CONTINUOUS = 'Continuous'
_DISCRETE = 'Discrete'
_BUILDING = 'BUILDING'
_SIZING_PERIODS = 'SIZINGPERIOD:DESIGNDAY'
_LOCATION = 'SITE:LOCATION'
_WINDOW_MATERIAL_SIMPLE = 'WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM'
_WINDOW = 'WINDOW'
_MATERIAL_ROOFVEGETATION = 'MATERIAL:ROOFVEGETATION'
_SIMPLE = 'Simple'
idf_surfaces = {
# todo: make an enum for all the surface types
cte.WALL: 'wall',
@ -91,7 +96,8 @@ class Idf:
'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._output_path = str(output_path.resolve())
self._output_file = str((output_path / f'{city.name}.idf').resolve())
@ -106,6 +112,12 @@ class Idf:
Numeric_Type=self._CONTINUOUS)
self._idf.newidfobject(self._SCHEDULE_LIMIT, Name=self._ON_OFF, Lower_Limit_Value=0, Upper_Limit_Value=1,
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()
@staticmethod
@ -298,13 +310,13 @@ class Idf:
_kwargs = {'Name': window_construction_name, 'Outside_Layer': material_name}
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']:
if zone.Name == thermal_zone.id:
if zone.Name == name:
return
# 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._add_heating_system(thermal_zone)
self._idf.newidfobject(self._ZONE, Name=name, Volume=thermal_zone.volume)
self._add_heating_system(thermal_zone, name)
def _add_thermostat(self, thermal_zone):
thermostat_name = f'Thermostat {thermal_zone.usage}'
@ -317,26 +329,26 @@ class Idf:
Heating_Setpoint_Schedule_Name=f'Heating 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]:
if air_system.Zone_Name == thermal_zone.id:
if air_system.Zone_Name == zone_name:
return
thermostat = self._add_thermostat(thermal_zone)
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}',
Heating_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)
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
fraction_radiant = thermal_zone.occupancy.sensible_radiative_internal_gain / \
(thermal_zone.occupancy.sensible_radiative_internal_gain +
thermal_zone.occupancy.sensible_convective_internal_gain)
self._idf.newidfobject(self._PEOPLE,
Name=f'{thermal_zone.id}_occupancy',
Zone_or_ZoneList_Name=thermal_zone.id,
Name=f'{zone_name}_occupancy',
Zone_or_ZoneList_Name=zone_name,
Number_of_People_Schedule_Name=f'Occupancy schedules {thermal_zone.usage}',
Number_of_People_Calculation_Method="People",
Number_of_People=number_of_people,
@ -344,21 +356,22 @@ class Idf:
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"]:
if zone.Name == f'{thermal_zone.id}_infiltration':
if zone.Name == f'{zone_name}_infiltration':
return
self._idf.newidfobject(self._INFILTRATION,
Name=f'{thermal_zone.id}_infiltration',
Zone_or_ZoneList_Name=thermal_zone.id,
Name=f'{zone_name}_infiltration',
Zone_or_ZoneList_Name=zone_name,
Schedule_Name=f'Infiltration schedules {thermal_zone.usage}',
Design_Flow_Rate_Calculation_Method='AirChanges/Hour',
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]:
building.Name = b.name
building.Name = f'Buildings in {name}'
building['Solar_Distribution'] = 'FullExterior'
def _remove_sizing_periods(self):
@ -370,14 +383,20 @@ class Idf:
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"
"""
self._remove_location()
self._remove_sizing_periods()
self._rename_building(self._city.name)
for building in self._city.buildings:
self._rename_building(building)
for internal_zone in building.internal_zones:
for thermal_zone in internal_zone.thermal_zones:
for thermal_boundary in thermal_zone.thermal_boundaries:
@ -387,6 +406,7 @@ class Idf:
for thermal_opening in thermal_boundary.thermal_openings:
self._add_window_construction_and_material(thermal_opening)
usage = thermal_zone.usage
if building.name in self._target_buildings or building.name in self._adjacent_buildings:
self._add_infiltration_schedules(thermal_zone)
self._add_schedules(usage, 'Occupancy', thermal_zone.occupancy.occupancy_schedules)
self._add_schedules(usage, 'HVAC AVAIL', thermal_zone.thermal_control.hvac_availability_schedules)
@ -394,19 +414,27 @@ class Idf:
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_heating_system(thermal_zone)
self._add_infiltration(thermal_zone)
self._add_occupancy(thermal_zone)
self._add_zone(thermal_zone, building.name)
self._add_heating_system(thermal_zone, building.name)
self._add_infiltration(thermal_zone, building.name)
self._add_occupancy(thermal_zone, building.name)
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:
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(
"OUTPUT:VARIABLE",
Variable_Name="Zone Ideal Loads Supply Air Total Heating Energy",
Reporting_Frequency="Hourly",
)
self._idf.newidfobject(
"OUTPUT:VARIABLE",
Variable_Name="Zone Ideal Loads Supply Air Total Cooling Energy",
@ -445,7 +473,25 @@ class Idf:
break
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 thermal_zone in internal_zone.thermal_zones:
for boundary in thermal_zone.thermal_boundaries:
@ -463,7 +509,7 @@ class Idf:
construction_name = boundary.construction_name
surface = self._idf.newidfobject(self._SURFACE, Name=f'{boundary.parent_surface.name}',
Surface_Type=idf_surface_type,
Zone_Name=thermal_zone.id,
Zone_Name=zone_name,
Construction_Name=construction_name,
Outside_Boundary_Condition=outside_boundary_condition,
Sun_Exposure=sun_exposure,
@ -533,5 +579,4 @@ class Idf:
soil.residual_volumetric_moisture_content,
Initial_Volumetric_Moisture_Content_of_the_Soil_Layer=
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
"""
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