From 5401064905f038f2b62eef81c184cb77f7481018 Mon Sep 17 00:00:00 2001 From: guille Date: Thu, 12 Sep 2024 06:57:15 +0200 Subject: [PATCH] cerc_idf basic implementation --- hub/exports/building_energy/cerc_idf.py | 422 ++++++++++++++++++ hub/exports/building_energy/idf.py | 23 +- .../building_energy/idf_files/base.idf | 62 +++ .../building_energy/idf_helper/__init__.py | 47 ++ .../energy_building_exports_factory.py | 13 + tests/test_exports.py | 3 +- 6 files changed, 563 insertions(+), 7 deletions(-) create mode 100644 hub/exports/building_energy/cerc_idf.py create mode 100644 hub/exports/building_energy/idf_files/base.idf create mode 100644 hub/exports/building_energy/idf_helper/__init__.py diff --git a/hub/exports/building_energy/cerc_idf.py b/hub/exports/building_energy/cerc_idf.py new file mode 100644 index 00000000..7d674f09 --- /dev/null +++ b/hub/exports/building_energy/cerc_idf.py @@ -0,0 +1,422 @@ +""" +Idf exports one building to idf format +SPDX - License - Identifier: LGPL - 3.0 - or -later +Copyright © 2022 Concordia CERC group +Project Coder Guille Guillermo.GutierrezMorote@concordia.ca +Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca + Oriol Gavalda Torrellas oriol.gavalda@concordia.ca +""" +import copy +from datetime import datetime +from pathlib import Path +import hub.helpers.constants as cte +import hub.exports.building_energy.idf_helper as idf_cte +from hub.city_model_structure.attributes.schedule import Schedule +from hub.city_model_structure.building_demand.layer import Layer + + +class CercIdf: + """ + Exports city to IDF + """ + + _schedules_added_to_idf = {} + _materials_added_to_idf = {} + _windows_added_to_idf = {'glazing_index': 0} + _constructions_added_to_idf = {} + + def __init__(self, city, output_path, idf_file_path, idd_file_path, epw_file_path, export_type="Surfaces", + target_buildings=None): + self._city = city + self._output_path = str(output_path.resolve()) + self._output_file_path = str((output_path / f'{city.name}.idf').resolve()) + self._output_schedules_path = str((output_path / 'schedules.idf').resolve()) + self._output_file_schedules_path = str((output_path / 'file_schedules.idf').resolve()) + self._output_solid_materials_path = str((output_path / 'solid_materials.idf').resolve()) + self._output_nomass_materials_path = str((output_path / 'nomass_materials.idf').resolve()) + self._output_window_materials_path = str((output_path / 'window_materials.idf').resolve()) + self._output_constructions_path = str((output_path / 'constructions.idf').resolve()) + self._output_surfaces_path = str((output_path / 'surfaces.idf').resolve()) + self._output_file_path = str((output_path / f'{city.name}.idf').resolve()) + + self._export_type = export_type + self._idd_file_path = str(idd_file_path) + self._idf_file_path = str(idf_file_path) + self._epw_file_path = str(epw_file_path) + self._target_buildings = target_buildings + self._adjacent_buildings = [] + + if target_buildings is None: + self._target_buildings = [building.name for building in self._city.buildings] + else: + for building_name in target_buildings: + building = city.city_object(building_name) + if building.neighbours is not None: + self._adjacent_buildings += building.neighbours + with open(self._idf_file_path, 'r') as base_idf: + lines = base_idf.readlines() + + self._output_schedules = open(self._output_schedules_path, 'w') + self._output_file_schedules = open(self._output_file_schedules_path, 'w') + self._output_solid_materials = open(self._output_solid_materials_path, 'w') + self._output_nomass_materials = open(self._output_nomass_materials_path, 'w') + self._output_window_materials = open(self._output_window_materials_path, 'w') + self._output_constructions = open(self._output_constructions_path, 'w') + self._output_surfaces = open(self._output_surfaces_path, 'w') + + with open(self._output_file_path, 'w') as self._idf_file: + self._idf_file.writelines(lines) + self._export() + + def __del__(self): + self._output_schedules.close() + self._output_file_schedules.close() + self._output_solid_materials.close() + self._output_nomass_materials.close() + + @staticmethod + def _write_to_idf_format(file, field, comment='', eol=','): + if comment != '': + comment = f' !- {comment}' + field = f' {field}{eol}'.ljust(26, ' ') + file.write(f'{field}{comment}\n') + else: + file.write(f'{field}{comment}') + + @staticmethod + def _matrix_to_list(points, lower_corner): + lower_x = lower_corner[0] + lower_y = lower_corner[1] + lower_z = lower_corner[2] + points_list = [] + for point in points: + point_tuple = (point[0] - lower_x, point[1] - lower_y, point[2] - lower_z) + points_list.append(point_tuple) + return points_list + + def _add_surfaces(self, building, zone_name): + # Verify if create building surfaces "by hand" it's faster wwr it's missing + zone_name = f'{zone_name}' + file = self._output_surfaces + for thermal_zone in building.thermal_zones_from_internal_zones: + for index, boundary in enumerate(thermal_zone.thermal_boundaries): + surface_type = idf_cte.idf_surfaces[boundary.parent_surface.type] + outside_boundary_condition = idf_cte.OUTDOORS + sun_exposure = idf_cte.SUN_EXPOSED + wind_exposure = idf_cte.WIND_EXPOSED + outside_boundary_condition_object = idf_cte.EMPTY + name = f'Building_{building.name}_surface_{index}' + construction_name = f'{boundary.construction_name} {boundary.parent_surface.type}' + space_name = idf_cte.EMPTY + if boundary.parent_surface.type == cte.GROUND: + outside_boundary_condition = idf_cte.GROUND + sun_exposure = idf_cte.NON_SUN_EXPOSED + wind_exposure = idf_cte.NON_WIND_EXPOSED + if boundary.parent_surface.percentage_shared is not None and boundary.parent_surface.percentage_shared > 0.5: + outside_boundary_condition_object = f'Building_{building.name}_surface_{index}' + outside_boundary_condition = idf_cte.SURFACE + sun_exposure = idf_cte.NON_SUN_EXPOSED + wind_exposure = idf_cte.NON_WIND_EXPOSED + self._write_to_idf_format(file, idf_cte.BUILDING_SURFACE) + self._write_to_idf_format(file, name, 'Name') + self._write_to_idf_format(file, surface_type, 'Surface Type') + self._write_to_idf_format(file, construction_name, 'Construction Name') + self._write_to_idf_format(file, zone_name, 'Zone Name') + self._write_to_idf_format(file, space_name, 'Space Name') + self._write_to_idf_format(file, outside_boundary_condition, 'Outside Boundary Condition') + self._write_to_idf_format(file, outside_boundary_condition_object, 'Outside Boundary Condition Object') + self._write_to_idf_format(file, sun_exposure, 'Sun Exposure') + self._write_to_idf_format(file, wind_exposure, 'Wind Exposure') + self._write_to_idf_format(file, idf_cte.AUTOCALCULATE, 'View Factor to Ground') + self._write_to_idf_format(file, idf_cte.AUTOCALCULATE, 'Number of Vertices') + coordinates = self._matrix_to_list(boundary.parent_surface.solid_polygon.coordinates, + self._city.lower_corner) + eol = ',' + for i, coordinate in enumerate(coordinates): + if i == len(coordinates) -1: + eol = ';' + self._write_to_idf_format(file, coordinate[0], f'Vertex {i} Xcoordinate') + self._write_to_idf_format(file, coordinate[1], f'Vertex {i} Ycoordinate') + self._write_to_idf_format(file, coordinate[2], f'Vertex {i} Zcoordinate', eol) + + @staticmethod + def _create_infiltration_schedules(thermal_zone): + _infiltration_schedules = [] + if thermal_zone.thermal_control is None: + return [] + for hvac_availability_schedule in thermal_zone.thermal_control.hvac_availability_schedules: + _schedule = Schedule() + _schedule.type = cte.INFILTRATION + _schedule.data_type = cte.FRACTION + _schedule.time_step = cte.HOUR + _schedule.time_range = cte.DAY + _schedule.day_types = copy.deepcopy(hvac_availability_schedule.day_types) + _infiltration_values = [] + for hvac_value in hvac_availability_schedule.values: + if hvac_value == 0: + _infiltration_values.append(1.0) + else: + if thermal_zone.infiltration_rate_system_off == 0: + _infiltration_values.append(0.0) + else: + _infiltration_values.append( + thermal_zone.infiltration_rate_system_on / thermal_zone.infiltration_rate_system_off) + _schedule.values = _infiltration_values + _infiltration_schedules.append(_schedule) + return _infiltration_schedules + + @staticmethod + def _create_ventilation_schedules(thermal_zone): + _ventilation_schedules = [] + if thermal_zone.thermal_control is None: + return [] + for hvac_availability_schedule in thermal_zone.thermal_control.hvac_availability_schedules: + _schedule = Schedule() + _schedule.type = cte.VENTILATION + _schedule.data_type = cte.FRACTION + _schedule.time_step = cte.HOUR + _schedule.time_range = cte.DAY + _schedule.day_types = copy.deepcopy(hvac_availability_schedule.day_types) + _ventilation_schedules = thermal_zone.thermal_control.hvac_availability_schedules + return _ventilation_schedules + + @staticmethod + def _create_constant_value_schedules(value, amount): + _schedule = Schedule() + _schedule.type = '' + _schedule.data_type = cte.ANY_NUMBER + _schedule.time_step = cte.HOUR + _schedule.time_range = cte.DAY + _schedule.day_types = ['monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + 'sunday', + 'holiday', + 'winter_design_day', + 'summer_design_day'] + _schedule.values = [value for _ in range(0, amount)] + return [_schedule] + + def _add_file_schedules(self, usage, schedule_type, schedules): + schedule_name = f'{schedule_type} schedules {usage}' + for schedule in schedules: + if schedule_name not in self._schedules_added_to_idf: + self._schedules_added_to_idf[schedule_name] = True + file_name = str((Path(self._output_path) / f'{schedule_type} schedules {usage.replace("/", "_")}.csv').resolve()) + with open(file_name, 'w', encoding='utf8') as file: + for value in schedule.values: + file.write(f'{str(value)},\n') + file = self._output_file_schedules + self._write_to_idf_format(file, idf_cte.FILE_SCHEDULE) + self._write_to_idf_format(file, schedule_name, 'Name') + self._write_to_idf_format(file, idf_cte.idf_type_limits[schedule.data_type], 'Schedule Type Limits Name') + self._write_to_idf_format(file, Path(file_name).name, 'File Name') + self._write_to_idf_format(file, 1, 'Column Number') + self._write_to_idf_format(file, 0, 'Rows to Skip at Top') + self._write_to_idf_format(file, 8760, 'Number of Hours of Data') + self._write_to_idf_format(file, 'Comma', 'Column Separator') + self._write_to_idf_format(file, 'No', 'Interpolate to Timestep') + self._write_to_idf_format(file, '60', 'Minutes per Item') + self._write_to_idf_format(file, 'Yes', 'Adjust Schedule for Daylight Savings', ';') + + def _add_idf_schedules(self, usage, schedule_type, schedules): + if len(schedules) < 1: + return + schedule_name = f'{schedule_type} schedules {usage}' + if schedule_name not in self._schedules_added_to_idf: + self._schedules_added_to_idf[schedule_name] = True + file = self._output_schedules + self._write_to_idf_format(file, idf_cte.COMPACT_SCHEDULE) + self._write_to_idf_format(file, schedule_name, 'Name') + self._write_to_idf_format(file, idf_cte.idf_type_limits[schedules[0].data_type], 'Schedule Type Limits Name') + self._write_to_idf_format(file, 'Through: 12/31', 'Field 1') + counter = 1 + for j, schedule in enumerate(schedules): + _val = schedule.values + _new_field = '' + for day_type in schedule.day_types: + _new_field += f' {idf_cte.idf_day_types[day_type]}' + self._write_to_idf_format(file, f'For:{_new_field}', f'Field {j * 25 + 2}') + counter += 1 + for i, _ in enumerate(_val): + self._write_to_idf_format(file, f'Until: {i + 1:02d}:00,{_val[i]}', f'Field {j * 25 + 3 + i}') + counter += 1 + self._write_to_idf_format(file, 'For AllOtherDays', f'Field {counter + 1}') + self._write_to_idf_format(file, 'Until: 24:00,0.0', f'Field {counter + 2}', ';') + + def _add_solid_material(self, layer): + file = self._output_solid_materials + self._write_to_idf_format(file, idf_cte.SOLID_MATERIAL) + self._write_to_idf_format(file, layer.material_name, 'Name') + self._write_to_idf_format(file, idf_cte.ROUGHNESS, 'Roughness') + self._write_to_idf_format(file, layer.thickness, 'Thickness') + self._write_to_idf_format(file, layer.conductivity, 'Conductivity') + self._write_to_idf_format(file, layer.density, 'Density') + self._write_to_idf_format(file, layer.specific_heat, 'Specific Heat') + self._write_to_idf_format(file, layer.thermal_absorptance, 'Thermal Absorptance') + self._write_to_idf_format(file, layer.solar_absorptance, 'Solar Absorptance') + self._write_to_idf_format(file, layer.visible_absorptance, 'Visible Absorptance', ';') + + def _add_nomass_material(self, layer): + file = self._output_nomass_materials + self._write_to_idf_format(file, idf_cte.NOMASS_MATERIAL) + self._write_to_idf_format(file, layer.material_name, 'Name') + self._write_to_idf_format(file, idf_cte.ROUGHNESS, 'Roughness') + self._write_to_idf_format(file, layer.thermal_resistance, 'Thermal Resistance') + self._write_to_idf_format(file, 0.9, 'Thermal Absorptance') + self._write_to_idf_format(file, 0.7, 'Solar Absorptance') + self._write_to_idf_format(file, 0.7, 'Visible Absorptance', ';') + + def _add_materials(self, thermal_boundary): + for layer in thermal_boundary.layers: + if layer.material_name not in self._materials_added_to_idf: + self._materials_added_to_idf[layer.material_name] = True + if layer.no_mass: + self._add_nomass_material(layer) + else: + self._add_solid_material(layer) + + def _add_default_material(self): + layer = Layer() + layer.material_name = 'DefaultMaterial' + layer.thickness = 0.1 + layer.conductivity = 0.1 + layer.density = 1000 + layer.specific_heat = 1000 + layer.thermal_absorptance = 0.9 + layer.solar_absorptance = 0.9 + layer.visible_absorptance = 0.7 + self._add_solid_material(layer) + return layer + + def _add_window_materials(self, thermal_opening): + name = f'{thermal_opening.overall_u_value}_{thermal_opening.g_value}' + if name not in self._windows_added_to_idf: + glazing_index = self._windows_added_to_idf['glazing_index'] + 1 + self._windows_added_to_idf[name] = True + self._windows_added_to_idf['glazing_index'] = glazing_index # increase the count + file = self._output_window_materials + self._write_to_idf_format(file, idf_cte.WINDOW_MATERIAL) + self._write_to_idf_format(file, f'glazing_{glazing_index}', 'Name') + self._write_to_idf_format(file, thermal_opening.overall_u_value, 'UFactor') + self._write_to_idf_format(file, thermal_opening.g_value, 'Solar Heat Gain Coefficient', ';') + + def _add_constructions(self, thermal_boundary): + """ + CONSTRUCTION, + 1000_1900_6 Roof, !- Name + Asphalt 1, !- Outside Layer + virtual_no_mass_13, !- Layer 2 + MW Glass Wool (rolls); !- Layer 3 + """ + if thermal_boundary.layers is None: + thermal_boundary.layers = [self._add_default_material()] + name = f'{thermal_boundary.construction_name} {thermal_boundary.parent_surface.type}' + + if name not in self._constructions_added_to_idf: + self._constructions_added_to_idf[name] = True + file = self._output_constructions + self._write_to_idf_format(file, idf_cte.CONSTRUCTION) + self._write_to_idf_format(file, name, 'Name') + eol = ',' + if len(thermal_boundary.layers) == 1: + eol = ';' + self._write_to_idf_format(file, thermal_boundary.layers[0].material_name, 'Outside Layer', eol) + for i in range(1, len(thermal_boundary.layers) - 1): + comment = f'Layer {i + 1}' + material_name = thermal_boundary.layers[i].material_name + if i == len(thermal_boundary.layers) - 2: + eol = ';' + self._write_to_idf_format(file, material_name, comment, eol) + + def _add_windows_constructions(self): + glazing_index = self._windows_added_to_idf['glazing_index'] + 1 + for i in range(1, glazing_index): + construction_name = f'window_construction_{i}' + material_name = f'glazing_{i}' + if construction_name not in self._constructions_added_to_idf: + self._constructions_added_to_idf[construction_name] = True + file = self._output_constructions + self._write_to_idf_format(file, idf_cte.CONSTRUCTION) + self._write_to_idf_format(file, construction_name, 'Name') + self._write_to_idf_format(file, material_name, 'Outside Layer', ';') + + def _merge_files(self): + self._output_schedules.flush() + with open(self._output_schedules_path, 'r') as file: + lines = file.readlines() + self._idf_file.writelines(lines) + self._output_file_schedules.flush() + with open(self._output_file_schedules_path, 'r') as file: + lines = file.readlines() + self._idf_file.writelines(lines) + self._output_solid_materials.flush() + with open(self._output_solid_materials_path, 'r') as file: + lines = file.readlines() + self._idf_file.writelines(lines) + self._output_nomass_materials.flush() + with open(self._output_nomass_materials_path, 'r') as file: + lines = file.readlines() + self._idf_file.writelines(lines) + self._output_window_materials.flush() + with open(self._output_window_materials_path, 'r') as file: + lines = file.readlines() + self._idf_file.writelines(lines) + self._output_constructions.flush() + with open(self._output_constructions_path, 'r') as file: + lines = file.readlines() + self._idf_file.writelines(lines) + self._output_surfaces.flush() + with open(self._output_surfaces_path, 'r') as file: + lines = file.readlines() + self._idf_file.writelines(lines) + + def _export(self): + for building in self._city.buildings: + is_target = building.name in self._target_buildings or building.name in self._adjacent_buildings + for internal_zone in building.internal_zones: + if internal_zone.thermal_zones_from_internal_zones is None: + self._target_buildings.remove(building.name) + is_target = False + continue + for thermal_zone in internal_zone.thermal_zones_from_internal_zones: + if is_target: + start = datetime.now() + service_temperature = thermal_zone.domestic_hot_water.service_temperature + usage = thermal_zone.usage_name + occ = thermal_zone.occupancy + if occ.occupancy_density == 0: + total_heat = 0 + else: + total_heat = ( + occ.sensible_convective_internal_gain + + occ.sensible_radiative_internal_gain + + occ.latent_internal_gain + ) / occ.occupancy_density + self._add_idf_schedules(usage, 'Infiltration', self._create_infiltration_schedules(thermal_zone)) + self._add_idf_schedules(usage, 'Ventilation', self._create_ventilation_schedules(thermal_zone)) + self._add_idf_schedules(usage, 'Occupancy', thermal_zone.occupancy.occupancy_schedules) + self._add_idf_schedules(usage, 'HVAC AVAIL', thermal_zone.thermal_control.hvac_availability_schedules) + self._add_idf_schedules(usage, 'Heating thermostat', thermal_zone.thermal_control.heating_set_point_schedules) + self._add_idf_schedules(usage, 'Cooling thermostat', thermal_zone.thermal_control.cooling_set_point_schedules) + self._add_idf_schedules(usage, 'Lighting', thermal_zone.lighting.schedules) + self._add_idf_schedules(usage, 'Appliance', thermal_zone.appliances.schedules) + self._add_idf_schedules(usage, 'DHW_prof', thermal_zone.domestic_hot_water.schedules) + self._add_idf_schedules(usage, 'DHW_temp', self._create_constant_value_schedules(service_temperature, 24)) + self._add_idf_schedules(usage, 'Activity Level', self._create_constant_value_schedules(total_heat, 24)) + self._add_file_schedules(usage, 'cold_temp', self._create_constant_value_schedules(building.cold_water_temperature[cte.HOUR], 24)) + for thermal_boundary in thermal_zone.thermal_boundaries: + self._add_materials(thermal_boundary) + self._add_constructions(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self._add_window_materials(thermal_opening) + self._add_windows_constructions() + if is_target: + self._add_surfaces(building, building.name) + + + # Merge files + self._merge_files() diff --git a/hub/exports/building_energy/idf.py b/hub/exports/building_energy/idf.py index 93b03cdf..35566279 100644 --- a/hub/exports/building_energy/idf.py +++ b/hub/exports/building_energy/idf.py @@ -107,6 +107,7 @@ class Idf: else: for building_name in target_buildings: building = city.city_object(building_name) + print('Name: ', building_name) if building.neighbours is not None: self._adjacent_buildings += building.neighbours self._export() @@ -520,7 +521,7 @@ class Idf: is_target = building.name in self._target_buildings or building.name in self._adjacent_buildings for internal_zone in building.internal_zones: if internal_zone.thermal_zones_from_internal_zones is None: - self._target_buildings.remoidf_surface_typeve(building.name) + self._target_buildings.remove(building.name) is_target = False continue for thermal_zone in internal_zone.thermal_zones_from_internal_zones: @@ -571,9 +572,8 @@ class Idf: if self._export_type == "Surfaces": if is_target: if building.thermal_zones_from_internal_zones is not None: - start = datetime.datetime.now() - self._add_surfaces(building, building.name) - print(f'add surfaces {datetime.datetime.now() - start}') + pass + # self._add_surfaces(building, building.name) else: self._add_pure_geometry(building, building.name) else: @@ -624,6 +624,10 @@ class Idf: self._idf.removeidfobject(window) self._idf.saveas(str(self._output_file)) + for building in self._city.buildings: + if self._export_type == "Surfaces": + if is_target and building.thermal_zones_from_internal_zones is not None: + self._add_surfaces(building, building.name) return self._idf def run(self): @@ -703,8 +707,13 @@ class Idf: self._idf.set_wwr(wwr) def _add_surfaces(self, building, zone_name): + start = datetime.datetime.now() + print(f'thermal_zones {len(building.thermal_zones_from_internal_zones)}') for thermal_zone in building.thermal_zones_from_internal_zones: + print(f'thermal zone: {datetime.datetime.now() - start}') for index, boundary in enumerate(thermal_zone.thermal_boundaries): + print(f'{index} boundary: {datetime.datetime.now() - start}') + idf_surface_type = self.idf_surfaces[boundary.parent_surface.type] outside_boundary_condition = 'Outdoors' sun_exposure = 'SunExposed' @@ -731,12 +740,14 @@ class Idf: else: construction_name = f'{boundary.construction_name} {boundary.parent_surface.type}' _kwargs['Construction_Name'] = construction_name - + start = datetime.datetime.now() surface = self._idf.newidfobject(self._SURFACE, **_kwargs) - + print(f'create surface: {datetime.datetime.now() - start}') coordinates = self._matrix_to_list(boundary.parent_surface.solid_polygon.coordinates, self._city.lower_corner) + surface.setcoords(coordinates) + print(f'set coords surface: {datetime.datetime.now() - start}') if self._lod >= 3: for internal_zone in building.internal_zones: diff --git a/hub/exports/building_energy/idf_files/base.idf b/hub/exports/building_energy/idf_files/base.idf new file mode 100644 index 00000000..80220bda --- /dev/null +++ b/hub/exports/building_energy/idf_files/base.idf @@ -0,0 +1,62 @@ +!- Linux Line endings + +Version, + 24.1; !- Version Identifier + +SimulationControl, + No, !- Do Zone Sizing Calculation + No, !- Do System Sizing Calculation + No, !- Do Plant Sizing Calculation + No, !- Run Simulation for Sizing Periods + Yes, !- Run Simulation for Weather File Run Periods + No, !- Do HVAC Sizing Simulation for Sizing Periods + 1; !- Maximum Number of HVAC Sizing Simulation Passes + +Building, + Buildings in b'Montreal', !- Name + 0, !- North Axis + Suburbs, !- Terrain + 0.04, !- Loads Convergence Tolerance Value + 0.4, !- Temperature Convergence Tolerance Value + FullExterior, !- Solar Distribution + 25, !- Maximum Number of Warmup Days + 6; !- Minimum Number of Warmup Days + +Timestep, + 4; !- Number of Timesteps per Hour + +RunPeriod, + Run Period 1, !- Name + 1, !- Begin Month + 1, !- Begin Day of Month + , !- Begin Year + 12, !- End Month + 31, !- End Day of Month + , !- End Year + Tuesday, !- Day of Week for Start Day + Yes, !- Use Weather File Holidays and Special Days + Yes, !- Use Weather File Daylight Saving Period + No, !- Apply Weekend Holiday Rule + Yes, !- Use Weather File Rain Indicators + Yes; !- Use Weather File Snow Indicators + +SCHEDULETYPELIMITS, + Any Number, !- Name + , !- Lower Limit Value + , !- Upper Limit Value + , !- Numeric Type + Dimensionless; !- Unit Type + +SCHEDULETYPELIMITS, + Fraction, !- Name + 0, !- Lower Limit Value + 1, !- Upper Limit Value + Continuous, !- Numeric Type + Dimensionless; !- Unit Type + +SCHEDULETYPELIMITS, + On/Off, !- Name + 0, !- Lower Limit Value + 1, !- Upper Limit Value + Discrete, !- Numeric Type + Dimensionless; !- Unit Type diff --git a/hub/exports/building_energy/idf_helper/__init__.py b/hub/exports/building_energy/idf_helper/__init__.py new file mode 100644 index 00000000..2ca3f526 --- /dev/null +++ b/hub/exports/building_energy/idf_helper/__init__.py @@ -0,0 +1,47 @@ +import hub.helpers.constants as cte + +BUILDING_SURFACE = '\nBUILDINGSURFACE:DETAILED,\n' +COMPACT_SCHEDULE = '\nSCHEDULE:COMPACT,\n' +FILE_SCHEDULE = '\nSCHEDULE:FILE,\n' +NOMASS_MATERIAL = '\nMATERIAL:NOMASS,\n' +SOLID_MATERIAL = '\nMATERIAL,\n' +WINDOW_MATERIAL = '\nWINDOWMATERIAL:SIMPLEGLAZINGSYSTEM,\n' +CONSTRUCTION = '\nCONSTRUCTION,\n' + +AUTOCALCULATE = 'autocalculate' +ROUGHNESS = 'MediumRough' +OUTDOORS = 'Outdoors' +GROUND = 'Ground' +SURFACE = 'Surface' +SUN_EXPOSED = 'SunExposed' +WIND_EXPOSED = 'WindExposed' +NON_SUN_EXPOSED = 'NoSun' +NON_WIND_EXPOSED = 'NoWind' +EMPTY = '' + +idf_surfaces = { + cte.WALL: 'wall', + cte.GROUND: 'floor', + cte.ROOF: 'roof' +} + +idf_type_limits = { + cte.ON_OFF: 'on/off', + cte.FRACTION: 'Fraction', + cte.ANY_NUMBER: 'Any Number', + cte.CONTINUOUS: 'Continuous', + cte.DISCRETE: 'Discrete' +} + +idf_day_types = { + cte.MONDAY: 'Monday', + cte.TUESDAY: 'Tuesday', + cte.WEDNESDAY: 'Wednesday', + cte.THURSDAY: 'Thursday', + cte.FRIDAY: 'Friday', + cte.SATURDAY: 'Saturday', + cte.SUNDAY: 'Sunday', + cte.HOLIDAY: 'Holidays', + cte.WINTER_DESIGN_DAY: 'WinterDesignDay', + cte.SUMMER_DESIGN_DAY: 'SummerDesignDay' +} \ No newline at end of file diff --git a/hub/exports/energy_building_exports_factory.py b/hub/exports/energy_building_exports_factory.py index 952e4a12..e474d775 100644 --- a/hub/exports/energy_building_exports_factory.py +++ b/hub/exports/energy_building_exports_factory.py @@ -11,6 +11,7 @@ import requests from hub.exports.building_energy.energy_ade import EnergyAde from hub.exports.building_energy.idf import Idf +from hub.exports.building_energy.cerc_idf import CercIdf from hub.exports.building_energy.insel.insel_monthly_energy_balance import InselMonthlyEnergyBalance from hub.helpers.utils import validate_import_export_type from hub.imports.weather.helpers.weather import Weather as wh @@ -60,6 +61,18 @@ class EnergyBuildingsExportsFactory: return Idf(self._city, self._path, (idf_data_path / 'Minimal.idf'), (idf_data_path / 'Energy+.idd'), weather_path, target_buildings=self._target_buildings) + @property + def _cerc_idf(self): + idf_data_path = (Path(__file__).parent / './building_energy/idf_files/').resolve() + url = wh().epw_file(self._city.region_code) + weather_path = (Path(__file__).parent.parent / f'data/weather/epw/{url.rsplit("/", 1)[1]}').resolve() + if not weather_path.exists(): + with open(weather_path, 'wb') as epw_file: + epw_file.write(requests.get(url, allow_redirects=True).content) + return CercIdf(self._city, self._path, (idf_data_path / 'base.idf'), (idf_data_path / 'Energy+.idd'), weather_path, + target_buildings=self._target_buildings) + + @property def _insel_monthly_energy_balance(self): """ diff --git a/tests/test_exports.py b/tests/test_exports.py index ab3943f7..d7b68b2b 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -144,6 +144,7 @@ class TestExports(TestCase): UsageFactory('nrcan', city).enrich() WeatherFactory('epw', city).enrich() try: - EnergyBuildingsExportsFactory('idf', city, self._output_path, target_buildings=[1]).export() + EnergyBuildingsExportsFactory('cerc_idf', city, self._output_path)._cerc_idf + except Exception: self.fail("Idf ExportsFactory raised ExceptionType unexpectedly!")