diff --git a/hub/exports/building_energy/cerc_idf.py b/hub/exports/building_energy/cerc_idf.py index 37978944..7892cbc4 100644 --- a/hub/exports/building_energy/cerc_idf.py +++ b/hub/exports/building_energy/cerc_idf.py @@ -8,6 +8,8 @@ Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concord """ import copy import os +import shutil +import subprocess from datetime import datetime import hub.exports.building_energy.idf_helper as idf_cte @@ -41,11 +43,12 @@ class CercIdf(IdfBase): _schedules_added_to_idf = {} _materials_added_to_idf = {} - _windows_added_to_idf = {'glazing_index': 0} + _windows_added_to_idf = {} _constructions_added_to_idf = {} _thermostat_added_to_idf = {} def __init__(self, city, output_path, idf_file_path, idd_file_path, epw_file_path, target_buildings=None): + self._start = datetime.now() super().__init__(city, output_path, idf_file_path, idd_file_path, epw_file_path, target_buildings) self._add_surfaces = IdfSurfaces.add self._add_file_schedule = IdfFileSchedule.add @@ -66,10 +69,13 @@ class CercIdf(IdfBase): self._add_shading = IdfShading.add self._add_windows = IdfWindow.add - with open(self._idf_file_path, 'r') as base_idf: + with open(self._idf_file_path, 'r', encoding='UTF-8') as base_idf: lines = base_idf.readlines() - - with open(self._output_file_path, 'w') as self._idf_file: + # Change city name + comment = f' !- Name' + field = f' Buildings in {self._city.name},'.ljust(26, ' ') + lines[15] = f'{field}{comment}\n' + with open(self._output_file_path, 'w', encoding='UTF-8') as self._idf_file: self._idf_file.writelines(lines) self._export() @@ -84,14 +90,14 @@ class CercIdf(IdfBase): for file in self._files.values(): file.close() for path in self._file_paths.values(): - with open(path, 'r') as file: + with open(path, 'r', encoding='UTF-8') as file: lines = file.readlines() self._idf_file.writelines(lines) for path in self._file_paths.values(): os.unlink(path) def _add_outputs(self): - with open(self._outputs_file_path, 'r') as base_idf: + with open(self._outputs_file_path, 'r', encoding='UTF-8') as base_idf: lines = base_idf.readlines() self._idf_file.writelines(lines) @@ -157,7 +163,7 @@ class CercIdf(IdfBase): return [_schedule] def _export(self): - start = datetime.now() + 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: @@ -198,8 +204,8 @@ class CercIdf(IdfBase): self._add_material(self, thermal_boundary) self._add_construction(self, thermal_boundary) for thermal_opening in thermal_boundary.thermal_openings: - self._add_windows_material(self, thermal_opening) - self._add_windows_constructions(self) + self._add_windows_material(self, thermal_boundary, thermal_opening) + self._add_windows_constructions(self, thermal_boundary) self._add_zone(self, thermal_zone, building.name) self._add_occupancy(self, thermal_zone, building.name) self._add_lighting(self, thermal_zone, building.name) @@ -211,7 +217,7 @@ class CercIdf(IdfBase): self._add_dhw(self, thermal_zone, building.name) if is_target: self._add_surfaces(self, building, building.name) - self._add_windows(self, building, building.name) + self._add_windows(self, building) else: self._add_shading(self, building) @@ -223,4 +229,20 @@ class CercIdf(IdfBase): # Merge files self._merge_files() self._add_outputs() - print(f'Export completed in: {datetime.now() - start}') + print(f'{len(self._city.buildings)} buildings export completed in: {datetime.now() - self._start}') + + @property + def _energy_plus(self): + return shutil.which('energyplus') + + def run(self): + cmd = [self._energy_plus, + '--weather', self._epw_file_path, + '--output-directory', self._output_path, + '--idd', self._idd_file_path, + '--expandobjects', + '--readvars', + '--output-prefix', f'{self._city.name}_', + self._output_file_path] + print(cmd) + subprocess.run(cmd, cwd=self._output_path) diff --git a/hub/exports/building_energy/idf.py b/hub/exports/building_energy/idf.py index 74be3179..e748d2eb 100644 --- a/hub/exports/building_energy/idf.py +++ b/hub/exports/building_energy/idf.py @@ -501,7 +501,7 @@ class Idf: ) def _rename_building(self, city_name): - name = str(str(city_name.encode("utf-8"))) + name = str(city_name.encode("utf-8")) for building in self._idf.idfobjects[self._BUILDING]: building.Name = f'Buildings in {name}' building['Solar_Distribution'] = 'FullExterior' @@ -586,8 +586,7 @@ class Idf: if self._export_type == "Surfaces": if is_target: if building.thermal_zones_from_internal_zones is not None: - pass - # self._add_surfaces(building, building.name) + self._add_surfaces(building, building.name) else: self._add_pure_geometry(building, building.name) else: @@ -733,13 +732,8 @@ 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' @@ -768,13 +762,9 @@ class Idf: _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: for thermal_zone in internal_zone.thermal_zones_from_internal_zones: @@ -786,7 +776,7 @@ class Idf: for surface in building.surfaces: if surface.type == cte.WALL: wwr = surface.associated_thermal_boundaries[0].window_ratio - self._idf.set_wwr(wwr, construction='window_construction_1') + #self._idf.set_wwr(wwr, construction='window_construction_1') def _add_windows_by_vertices(self, boundary): raise NotImplementedError diff --git a/hub/exports/building_energy/idf_files/base.idf b/hub/exports/building_energy/idf_files/base.idf index 80220bda..855b7c48 100644 --- a/hub/exports/building_energy/idf_files/base.idf +++ b/hub/exports/building_energy/idf_files/base.idf @@ -13,7 +13,7 @@ SimulationControl, 1; !- Maximum Number of HVAC Sizing Simulation Passes Building, - Buildings in b'Montreal', !- Name + Buildings in #CITY#, !- Name 0, !- North Axis Suburbs, !- Terrain 0.04, !- Loads Convergence Tolerance Value diff --git a/hub/exports/building_energy/idf_helper/__init__.py b/hub/exports/building_energy/idf_helper/__init__.py index fed4f458..f55f00c6 100644 --- a/hub/exports/building_energy/idf_helper/__init__.py +++ b/hub/exports/building_energy/idf_helper/__init__.py @@ -1,6 +1,7 @@ import hub.helpers.constants as cte BUILDING_SURFACE = '\nBUILDINGSURFACE:DETAILED,\n' +WINDOW_SURFACE = '\nFENESTRATIONSURFACE:DETAILED,\n' COMPACT_SCHEDULE = '\nSCHEDULE:COMPACT,\n' FILE_SCHEDULE = '\nSCHEDULE:FILE,\n' NOMASS_MATERIAL = '\nMATERIAL:NOMASS,\n' diff --git a/hub/exports/building_energy/idf_helper/idf_base.py b/hub/exports/building_energy/idf_helper/idf_base.py index cbd0e4ae..ea6fe237 100644 --- a/hub/exports/building_energy/idf_helper/idf_base.py +++ b/hub/exports/building_energy/idf_helper/idf_base.py @@ -31,7 +31,7 @@ class IdfBase: } self._files = {} for key, value in self._file_paths.items(): - self._files[key] = open(value, 'w') + self._files[key] = open(value, 'w', encoding='UTF-8') self._idd_file_path = str(idd_file_path) self._idf_file_path = str(idf_file_path) diff --git a/hub/exports/building_energy/idf_helper/idf_surfaces.py b/hub/exports/building_energy/idf_helper/idf_surfaces.py index a6cd9a74..cdae4f31 100644 --- a/hub/exports/building_energy/idf_helper/idf_surfaces.py +++ b/hub/exports/building_energy/idf_helper/idf_surfaces.py @@ -6,7 +6,6 @@ from hub.exports.building_energy.idf_helper.idf_base import IdfBase class IdfSurfaces(IdfBase): @staticmethod def add(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._files['surfaces'] for thermal_zone in building.thermal_zones_from_internal_zones: diff --git a/hub/exports/building_energy/idf_helper/idf_window.py b/hub/exports/building_energy/idf_helper/idf_window.py index 61cb7f49..4e2b2645 100644 --- a/hub/exports/building_energy/idf_helper/idf_window.py +++ b/hub/exports/building_energy/idf_helper/idf_window.py @@ -1,9 +1,64 @@ +import logging + import hub.exports.building_energy.idf_helper as idf_cte import hub.helpers.constants as cte from hub.exports.building_energy.idf_helper.idf_base import IdfBase class IdfWindow(IdfBase): + @staticmethod - def add(self, building, zone_name): - pass \ No newline at end of file + def _to_window_surface(self, surface): + window_ratio = surface.associated_thermal_boundaries[0].window_ratio + x = 0 + y = 1 + z = 2 + coordinates = self._matrix_to_list(surface.solid_polygon.coordinates, self._city.lower_corner) + min_z = surface.lower_corner[z] + max_z = surface.upper_corner[z] + middle = (max_z - min_z) / 2 + distance = (max_z - min_z) * window_ratio + new_max_z = middle + distance / 2 + new_min_z = middle - distance / 2 + for index, coordinate in enumerate(coordinates): + if coordinate[z] == max_z: + coordinates[index] = (coordinate[x], coordinate[y], new_max_z) + elif coordinate[z] == min_z: + coordinates[index] = (coordinate[x], coordinate[y], new_min_z) + else: + logging.warning('Z coordinate not in top or bottom during window creation') + return coordinates + + @staticmethod + def add(self, building): + file = self._files['surfaces'] + for thermal_zone in building.thermal_zones_from_internal_zones: + for index, boundary in enumerate(thermal_zone.thermal_boundaries): + building_surface_name = f'Building_{building.name}_surface_{index}' + is_exposed = boundary.parent_surface.type == cte.WALL + if boundary.parent_surface.percentage_shared is not None and boundary.parent_surface.percentage_shared > 0.5 or boundary.window_ratio == 0: + is_exposed = False + if not is_exposed: + continue + name = f'Building_{building.name}_window_{index}' + construction_name = f'{boundary.construction_name}_window' + self._write_to_idf_format(file, idf_cte.WINDOW_SURFACE) + self._write_to_idf_format(file, name, 'Name') + self._write_to_idf_format(file, 'Window', 'Surface Type') + self._write_to_idf_format(file, construction_name, 'Construction Name') + self._write_to_idf_format(file, building_surface_name, 'Building Surface Name') + self._write_to_idf_format(file, idf_cte.EMPTY, 'Outside Boundary Condition Object') + self._write_to_idf_format(file, idf_cte.AUTOCALCULATE, 'View Factor to Ground') + self._write_to_idf_format(file, idf_cte.EMPTY, 'Frame and Divider Name') + self._write_to_idf_format(file, '1.0', 'Multiplier') + self._write_to_idf_format(file, idf_cte.AUTOCALCULATE, 'Number of Vertices') + coordinates = IdfWindow._to_window_surface(self, boundary.parent_surface) + eol = ',' + coordinates_length = len(coordinates) + for i, coordinate in enumerate(coordinates): + vertex = i + 1 + if vertex == coordinates_length: + eol = ';' + self._write_to_idf_format(file, coordinate[0], f'Vertex {vertex} Xcoordinate') + self._write_to_idf_format(file, coordinate[1], f'Vertex {vertex} Ycoordinate') + self._write_to_idf_format(file, coordinate[2], f'Vertex {vertex} Zcoordinate', eol) diff --git a/hub/exports/building_energy/idf_helper/idf_windows_constructions.py b/hub/exports/building_energy/idf_helper/idf_windows_constructions.py index 44c454ef..c5a4144c 100644 --- a/hub/exports/building_energy/idf_helper/idf_windows_constructions.py +++ b/hub/exports/building_energy/idf_helper/idf_windows_constructions.py @@ -4,14 +4,14 @@ from hub.exports.building_energy.idf_helper.idf_base import IdfBase class IdfWindowsConstructions(IdfBase): @staticmethod - def add(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._files['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 add(self, thermal_boundary): + name = f'{thermal_boundary.construction_name}_window' + if name not in self._windows_added_to_idf: + return # Material not added or already assigned to construction + construction_name = f'{thermal_boundary.construction_name}_window_construction' + if construction_name not in self._constructions_added_to_idf: + self._constructions_added_to_idf[construction_name] = True + file = self._files['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, name, 'Outside Layer', ';') diff --git a/hub/exports/building_energy/idf_helper/idf_windows_material.py b/hub/exports/building_energy/idf_helper/idf_windows_material.py index 9b9c8e28..2c7b30b2 100644 --- a/hub/exports/building_energy/idf_helper/idf_windows_material.py +++ b/hub/exports/building_energy/idf_helper/idf_windows_material.py @@ -4,14 +4,12 @@ from hub.exports.building_energy.idf_helper.idf_base import IdfBase class IdfWindowsMaterial(IdfBase): @staticmethod - def add(self, thermal_opening): - name = f'{thermal_opening.overall_u_value}_{thermal_opening.g_value}' + def add(self, thermal_boundary, thermal_opening): + name = f'{thermal_boundary.construction_name}_window' 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._files['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, name, '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', ';') \ No newline at end of file + self._write_to_idf_format(file, thermal_opening.g_value, 'Solar Heat Gain Coefficient', ';') diff --git a/tests/test_exports.py b/tests/test_exports.py index ac881960..0d672556 100644 --- a/tests/test_exports.py +++ b/tests/test_exports.py @@ -128,15 +128,30 @@ class TestExports(TestCase): """ export to IDF """ + "file = '1588_v1.geojson'" file = 'test.geojson' file_path = (self._example_path / file).resolve() + city = GeometryFactory('geojson', path=file_path, height_field='citygml_me', year_of_construction_field='ANNEE_CONS', function_field='CODE_UTILI', function_to_hub=Dictionaries().montreal_function_to_hub_function).city - + """=-043 + "name": "01043081", + "address": "avenue de l' H\u00f4tel-de-Ville (MTL) 3751", + "function": "6911", + "height": 21, + "year_of_construction": 1964, + + city = GeometryFactory('geojson', + path=file_path, + height_field='height', + year_of_construction_field='year_of_construction', + function_field='function', + function_to_hub=Dictionaries().montreal_function_to_hub_function).city + """ self.assertIsNotNone(city, 'city is none') #EnergyBuildingsExportsFactory('idf', city, self._output_path).export() ConstructionFactory('nrcan', city).enrich() @@ -144,7 +159,10 @@ class TestExports(TestCase): UsageFactory('nrcan', city).enrich() WeatherFactory('epw', city).enrich() try: - EnergyBuildingsExportsFactory('cerc_idf', city, self._output_path)._cerc_idf + idf = EnergyBuildingsExportsFactory('idf', city, self._output_path).export() + # idf.run() + idf = EnergyBuildingsExportsFactory('cerc_idf', city, self._output_path).export() + idf.run() except Exception: self.fail("Idf ExportsFactory raised ExceptionType unexpectedly!")