diff --git a/city_model_structure/building.py b/city_model_structure/building.py index 0728ad2f..ad94a754 100644 --- a/city_model_structure/building.py +++ b/city_model_structure/building.py @@ -113,6 +113,7 @@ class Building(CityObject): """ if len(self._usage_zones) == 0: for thermal_zone in self.thermal_zones: + print('thermal zone') self._usage_zones.extend(thermal_zone.usage_zones) return self._usage_zones diff --git a/city_model_structure/building_demand/thermal_opening.py b/city_model_structure/building_demand/thermal_opening.py index 5eb6f4b0..01aebd2d 100644 --- a/city_model_structure/building_demand/thermal_opening.py +++ b/city_model_structure/building_demand/thermal_opening.py @@ -127,6 +127,8 @@ class ThermalOpening: if self._overall_u_value is None and self.conductivity is not None: h_i = self.hi h_e = self.he + h_i = 1 + h_e = 1 r_value = 1 / h_i + 1 / h_e + float(self.conductivity) / float(self.thickness) self._overall_u_value = 1 / r_value diff --git a/city_model_structure/building_demand/thermal_zone.py b/city_model_structure/building_demand/thermal_zone.py index 40aee9ca..e469ef98 100644 --- a/city_model_structure/building_demand/thermal_zone.py +++ b/city_model_structure/building_demand/thermal_zone.py @@ -25,7 +25,7 @@ class ThermalZone: self._indirectly_heated_area_ratio = None self._infiltration_rate_system_on = None self._infiltration_rate_system_off = None - self._usage_zones = None + self._usage_zones = [] self._volume = volume self._volume_geometry = None self._id = None diff --git a/exports/exports_factory.py b/exports/exports_factory.py index eaa07e0f..0f386d37 100644 --- a/exports/exports_factory.py +++ b/exports/exports_factory.py @@ -9,6 +9,7 @@ from exports.formats.obj import Obj from exports.formats.energy_ade import EnergyAde from exports.formats.simplified_radiosity_algorithm import SimplifiedRadiosityAlgorithm from exports.formats.idf import Idf +from pathlib import Path class ExportsFactory: @@ -66,7 +67,10 @@ class ExportsFactory: Export the city to Energy+ idf format :return: """ - return Idf() + # todo: this need to be generalized + data_path = Path('../libs_Final/tests/tests_data').resolve() + return Idf(self._city, self._path, (data_path / f'minimal.idf').resolve(), (data_path / f'energy+.idd').resolve(), + (data_path / f'montreal.epw').resolve()) @property def _sra(self): diff --git a/exports/formats/idf.py b/exports/formats/idf.py index 842ae3ce..f4c40893 100644 --- a/exports/formats/idf.py +++ b/exports/formats/idf.py @@ -4,12 +4,7 @@ SPDX - License - Identifier: LGPL - 3.0 - or -later Copyright © 2020 Project Author Soroush Samareh Abolhassani - soroush.samarehabolhassani@mail.concordia.ca """ from geomeppy import IDF -import os -import esoreader from pathlib import Path -from city_model_structure.city import City - - class Idf: _THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT' _IDEAL_LOAD_AIR_SYSTEM = 'HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM' @@ -18,9 +13,13 @@ class Idf: _MATERIAL = 'MATERIAL' _MATERIAL_NOMASS = 'MATERIAL:NOMASS' _ROUGHNESS = "MediumRough" - _OCCUPANCY_HOURLY_SCHEDULE = "SCHEDULE:DAY:HOURLY" + _HOURLY_SCHEDULE = "SCHEDULE:DAY:HOURLY" _ZONE = "ZONE" _LIGHTS = "LIGHTS" + _PEOPLE = "PEOPLE" + _ELECTRIC_EQUIPMEN = "ELECTRICEQUIPMENT" + _INFILTRATION = "ZONEINFILTRATION:DESIGNFLOWRATE" + _BUILDING_SURFACE = "BuildingSurfaceDetailed" idf_surfaces = { # todo: make an enum for all the surface types @@ -33,21 +32,32 @@ class Idf: 'residential': 'residential_building' } - def __init__(self, city, idf_file_path, idd_file_path, epw_file_path): + def __init__(self, city, output_path, idf_file_path, idd_file_path, epw_file_path): self._city = city + self._output_path = str(output_path.resolve()) + print(self._output_path) self._idd_file_path = str(idd_file_path) self._idf_file_path = str(idf_file_path) self._epw_file_path = str(epw_file_path) IDF.setiddname(self._idd_file_path) self._idf = IDF(self._idf_file_path, self._epw_file_path) + self._export() - def _add_components(self, building): - for zone in building.usage_zones: - self._add_occupancy_schedule(zone) - self._add_heating_system(zone) - for zone in building.thermal_zones: - for boundary in zone.bounded: - self._add_construction(boundary) + @staticmethod + def _matrix_to_list(points): + points_list = [] + for point in points: + point_tuple = (point[0], point[1], point[2]) + points_list.append(point_tuple) + return points_list + + @staticmethod + def _matrix_to_2d_list(points): + points_list = [] + for point in points: + point_tuple = (point[0], point[1]) + points_list.append(point_tuple) + return points_list def _add_material(self, layer): for material in self._idf.idfobjects[self._MATERIAL]: @@ -79,40 +89,43 @@ class Idf: ) def _add_schedule(self, usage_zone, schedule_type): - for schedule in self._idf.idfobjects[schedule_type]: + for schedule in self._idf.idfobjects[self._HOURLY_SCHEDULE]: if schedule.Name == f'{schedule_type} schedules {usage_zone.usage}': return - schedule_occupancy = self._idf.newidfobject(self._OCCUPANCY_HOURLY_SCHEDULE) - schedule_occupancy.Name = f'occupant schedules {usage_zone.usage}' - schedule_occupancy.Hour_1 = usage_zone.schedules[schedule_type] .occupants.occupant_schedule[0] - schedule_occupancy.Hour_2 = usage_zone.occupants.occupant_schedule[1] - schedule_occupancy.Hour_3 = usage_zone.occupants.occupant_schedule[2] - schedule_occupancy.Hour_4 = usage_zone.occupants.occupant_schedule[3] - schedule_occupancy.Hour_5 = usage_zone.occupants.occupant_schedule[4] - schedule_occupancy.Hour_6 = usage_zone.occupants.occupant_schedule[5] - schedule_occupancy.Hour_7 = usage_zone.occupants.occupant_schedule[6] - schedule_occupancy.Hour_8 = usage_zone.occupants.occupant_schedule[7] - schedule_occupancy.Hour_9 = usage_zone.occupants.occupant_schedule[8] - schedule_occupancy.Hour_10 = usage_zone.occupants.occupant_schedule[9] - schedule_occupancy.Hour_11 = usage_zone.occupants.occupant_schedule[10] - schedule_occupancy.Hour_12 = usage_zone.occupants.occupant_schedule[11] - schedule_occupancy.Hour_13 = usage_zone.occupants.occupant_schedule[12] - schedule_occupancy.Hour_14 = usage_zone.occupants.occupant_schedule[13] - schedule_occupancy.Hour_15 = usage_zone.occupants.occupant_schedule[14] - schedule_occupancy.Hour_16 = usage_zone.occupants.occupant_schedule[15] - schedule_occupancy.Hour_17 = usage_zone.occupants.occupant_schedule[16] - schedule_occupancy.Hour_18 = usage_zone.occupants.occupant_schedule[17] - schedule_occupancy.Hour_19 = usage_zone.occupants.occupant_schedule[18] - schedule_occupancy.Hour_20 = usage_zone.occupants.occupant_schedule[19] - schedule_occupancy.Hour_21 = usage_zone.occupants.occupant_schedule[20] - schedule_occupancy.Hour_22 = usage_zone.occupants.occupant_schedule[21] - schedule_occupancy.Hour_23 = usage_zone.occupants.occupant_schedule[22] - schedule_occupancy.Hour_24 = usage_zone.occupants.occupant_schedule[23] + schedule = self._idf.newidfobject(self._HOURLY_SCHEDULE) + schedule.Name = f'{schedule_type} schedules {usage_zone.usage}' + schedule.Schedule_Type_Limits_Name = 'Any Number' + + schedule.Hour_1 = usage_zone.schedules[schedule_type]["WD"][0] + schedule.Hour_2 = usage_zone.schedules[schedule_type]["WD"][1] + schedule.Hour_3 = usage_zone.schedules[schedule_type]["WD"][2] + schedule.Hour_4 = usage_zone.schedules[schedule_type]["WD"][3] + schedule.Hour_5 = usage_zone.schedules[schedule_type]["WD"][4] + schedule.Hour_6 = usage_zone.schedules[schedule_type]["WD"][5] + schedule.Hour_7 = usage_zone.schedules[schedule_type]["WD"][6] + schedule.Hour_8 = usage_zone.schedules[schedule_type]["WD"][7] + schedule.Hour_9 = usage_zone.schedules[schedule_type]["WD"][8] + schedule.Hour_10 = usage_zone.schedules[schedule_type]["WD"][9] + schedule.Hour_11 = usage_zone.schedules[schedule_type]["WD"][10] + schedule.Hour_12 = usage_zone.schedules[schedule_type]["WD"][11] + schedule.Hour_13 = usage_zone.schedules[schedule_type]["WD"][12] + schedule.Hour_14 = usage_zone.schedules[schedule_type]["WD"][13] + schedule.Hour_15 = usage_zone.schedules[schedule_type]["WD"][14] + schedule.Hour_16 = usage_zone.schedules[schedule_type]["WD"][15] + schedule.Hour_17 = usage_zone.schedules[schedule_type]["WD"][16] + schedule.Hour_18 = usage_zone.schedules[schedule_type]["WD"][17] + schedule.Hour_19 = usage_zone.schedules[schedule_type]["WD"][18] + schedule.Hour_20 = usage_zone.schedules[schedule_type]["WD"][19] + schedule.Hour_21 = usage_zone.schedules[schedule_type]["WD"][20] + schedule.Hour_22 = usage_zone.schedules[schedule_type]["WD"][21] + schedule.Hour_23 = usage_zone.schedules[schedule_type]["WD"][22] + schedule.Hour_24 = usage_zone.schedules[schedule_type]["WD"][23] def _add_construction(self, thermal_boundary): for construction in self._idf.idfobjects[self._CONSTRUCTION]: if construction.Name == thermal_boundary.construction_name: return + for layer in thermal_boundary.layers: self._add_material(layer) layers = thermal_boundary.layers @@ -129,8 +142,6 @@ class Idf: # todo: what does we need to define a zone in energy plus? self._idf.newidfobject(self._ZONE, Name=usage_zone.id) self._add_heating_system(usage_zone) - lighting = self._add_lighting(usage_zone) - lighting.Zone_or_ZoneList_Name= usage_zone.id, def _add_thermostat(self, usage_zone): thermostat_name = f'Thermostat {usage_zone.usage}' @@ -149,141 +160,89 @@ class Idf: Zone_Name=usage_zone.id, Template_Thermostat_Name=thermostat.Name) - def _add_lighting(self, usage_zone): - for lights in self._idf.idfobjects[self._LIGHTS]: - if lights.Name == f'{usage_zone.usage}_lighting': - return - - self._idf.newidfobject(self._LIGHTS, - Name=f'{usage_zone.id}_lighting', - - Schedule_Name="Lighting_Weekday", # todo: from where? - Design_Level_Calculation_Method='Watts/Area', # todo: to constant - Watts_per_Zone_Floor_Area=15, # todo: from usage library - Return_Air_Fraction=0, # todo: to constant/document it. - Fraction_Radiant=0.7, # todo: from usage library - Fraction_Visible=0.2, # todo: ???? - Fraction_Replaceable=1, # todo: to constant/document it. - Return_Air_Fraction_Calculated_from_Plenum_Temperature='No' # todo: to constant/document it. - ) - - def add_occupancy(self): - for zone in self._idf.idfobjects["ZONE"]: - self._idf.newidfobject("PEOPLE", - Name=zone.Name + "_" + "schedules", - Zone_or_ZoneList_Name=zone.Name, - Number_of_People_Schedule_Name='occupant schedules', + def _add_occupancy(self, usage_zone): + self._idf.newidfobject(self._PEOPLE, + Name=f'{usage_zone.id}_occupancy', + Zone_or_ZoneList_Name=usage_zone.id, + Number_of_People_Schedule_Name=f'Occupancy schedules {usage_zone.usage}', Number_of_People_Calculation_Method="People", - Number_of_People=500, - Fraction_Radiant=0.3, + Number_of_People=500, # todo: get people from where? + Fraction_Radiant=0.3, # todo: howto get this from InternalGains Activity_Level_Schedule_Name='occupant schedules' ) - def add_equipment(self): - for zone in self._idf.idfobjects["ZONE"]: - self._idf.newidfobject("ELECTRICEQUIPMENT", - Name=zone.Name + "_" + 'electricload', - Zone_or_ZoneList_Name=zone.Name, - Schedule_Name='ElectricalEquipment', - Design_Level_Calculation_Method='EquipmentLevel', - Design_Level=566000 - ) + def _add_equipment(self, usage_zone): + self._idf.newidfobject(self._ELECTRIC_EQUIPMENT, + Name=f'{usage_zone.id}_electricload', + Zone_or_ZoneList_Name=usage_zone.id, + Schedule_Name=f'Electrical schedules {usage_zone.usage}', # todo: add electrical schedules + Design_Level_Calculation_Method='EquipmentLevel', + Design_Level=566000 # todo: change it from usage catalog + ) - def add_infiltration(self): + def _add_infiltration(self, usage_zone): for zone in self._idf.idfobjects["ZONE"]: - self._idf.newidfobject("ZONEINFILTRATION:DESIGNFLOWRATE", - Name=zone.Name + "_" + "infiltration", - Zone_or_ZoneList_Name=zone.Name, - Schedule_Name='Infiltration_schedule', + self._idf.newidfobject(self._INFILTRATION, + Name=f'{usage_zone.id}_infiltration', + Zone_or_ZoneList_Name=usage_zone.id, + Schedule_Name=f'Infiltration schedules {usage_zone.usage}', Design_Flow_Rate_Calculation_Method='AirChanges/Hour', - Air_Changes_per_Hour=0.35, - Constant_Term_Coefficient=0.606, - Temperature_Term_Coefficient=3.6359996E-02, - Velocity_Term_Coefficient=0.1177165, - Velocity_Squared_Term_Coefficient=0.0000000E+00 + Air_Changes_per_Hour=0.35, # todo: change it from usage catalog + Constant_Term_Coefficient=0.606, # todo: change it from usage catalog + Temperature_Term_Coefficient=3.6359996E-02, # todo: change it from usage catalog + Velocity_Term_Coefficient=0.1177165, # todo: change it from usage catalog + Velocity_Squared_Term_Coefficient=0.0000000E+00 # todo: change it from usage catalog ) - def run(self, energy_plus_path, output_directory, window_ratio=0.35, display_render=False, output_prefix=None, - keep_file=None): - # todo: doesn't really make sense to run energy+ from here, should just return the path to files. - self._idf.set_default_constructions() - # todo: calculate window ratio in the workflow - self._idf.set_wwr(window_ratio, construction="Project External Window") - self._idf.translate_to_origin() - if display_render: - self._idf.view_model() - # Run - self._idf.newidfobject("OUTPUT:METER", Key_Name="Heating:DistrictHeating", Reporting_Frequency="hourly") - self._idf.newidfobject("OUTPUT:METER", Key_Name="Cooling:DistrictCooling", Reporting_Frequency="hourly") - idf_path = None - if keep_file is not None: - idf_path = (keep_file / 'in.idf').resolve() - self._idf.saveas(str(idf_path)) - if idf_path is None: - idf_path = (Path(__file__).parent / 'in.idf').resolve() - - # There is a bug in the IDF class, when called, it return an error, as a work around we call call energy+ directly - run_command = f"{energy_plus_path} --weather {self._epw_file_path} --output-directory {output_directory} --idd " \ - f"{self._idd_file_path} --expandobjects --output-prefix {output_prefix} {idf_path}" - os.system(run_command) - - if keep_file is None: - os.remove(idf_path) - return - - @staticmethod - def read_eso(eso_file_path): - # todo: the heating and cooling values need to go into the city_model_structure - dd, data = esoreader.read(eso_file_path) - list_values = [v for v in data.values()] - heating = [(float(x)) / 3600000.0 for x in list_values[0]] - cooling = [(float(x)) / 3600000.0 for x in list_values[1]] - return heating, cooling - - @staticmethod - def _matrix_to_list(points): - points_list = [] - for point in points: - point_tuple = (point[0], point[1], point[2]) - points_list.append(point_tuple) - return points_list - - @staticmethod - def _matrix_to_2d_list(points): - points_list = [] - for point in points: - point_tuple = (point[0], point[1]) - points_list.append(point_tuple) - return points_list - - def _export(self, city, export_type="Surfaces"): - for building in city.buildings: + def _export(self, export_type="Surfaces"): + """ + Export the idf file into the given path + export type = "Surfaces|Block" + """ + print("called") + for building in self._city.buildings: + print('add building') + for usage_zone in building.usage_zones: + self._add_schedule(usage_zone, "Infiltration") + self._add_schedule(usage_zone, "Lights") + self._add_schedule(usage_zone, "Occupancy") + self._add_zone(usage_zone) + self._add_heating_system(usage_zone) + self._add_construction(usage_zone) + print('zone construction') + print('add surfaces') if export_type == "Surfaces": self._add_surfaces(building) - else + else: self._add_block(building) - + print(' =out path', str(self._output_path)) + self._idf.saveas(str(self._output_path)) def _add_block(self, building): _points = self._matrix_to_2d_list(building.foot_print.coordinates) self._idf.add_block(name=building.name, coordinates=_points, height=building.max_height, num_stories=int(building.storeys_above_ground)) + for surface in self._idf.idfobjects[self._SURFACE]: + for thermal_zone in building.thermal_zones: + for boundary in thermal_zone.bounded: + if surface.Type == self.idf_surfaces[boundary.surface.type]: + surface.Construction_Name = boundary.construction_name + break + for usage_zone in thermal_zone.usage_zones: + surface.Zone_Name = usage_zone.id + break + break self._idf.intersect_match() def _add_surfaces(self, building): - index = 0 - for zone in building.thermal_zones: - zone_name = f'Building {building.name} usage zone {index}' - self._idf.newidfobject('ZONE', Name=zone_name) - for boundary in zone.bounded: - self._add_construction(boundary) + for thermal_zone in building.thermal_zones: + for boundary in thermal_zone.bounded: idf_surface = self.idf_surfaces[boundary.surface.type] - surface = self._idf.newidfobject(self._SURFACE, Name=f'{building.name}-{boundary.surface.name}', - Surface_Type=idf_surface, Zone_Name=zone_name, - Construction_Name=boundary.construction_name) - coordinates = self._matrix_to_list(boundary.surface.coordinates) - surface.setcoords(coordinates) - index += 1 - self._add_heating_system(building) - self._idf.intersect_match() + for usage_zone in thermal_zone.usage_zones: + surface = self._idf.newidfobject(self._SURFACE, Name=f'{boundary.surface.name}', + Surface_Type=idf_surface, Zone_Name=usage_zone.id, + Construction_Name=boundary.construction_name) + coordinates = self._matrix_to_list(boundary.surface.coordinates) + surface.setcoords(coordinates) + self._idf.intersect_match() diff --git a/imports/construction/helpers/construction_helper.py b/imports/construction/helpers/construction_helper.py index f1dca0cf..81368a00 100644 --- a/imports/construction/helpers/construction_helper.py +++ b/imports/construction/helpers/construction_helper.py @@ -73,7 +73,7 @@ class ConstructionHelper: cte.SECONDARY_SCHOOL: 'secondary school', cte.OFFICE: 'office', cte.LARGE_OFFICE: 'large office', - cte.OFFICE_WORKSHOP: 'large office' + cte.OFFICE_WORKSHOP: 'residential' } nrcan_function_default_value = 'residential' nrcan_window_types = [cte.WINDOW] diff --git a/non_functional_tests/tests_data/C40_Final.gml b/non_functional_tests/tests_data/C40_Final.gml index 60c6fc69..8c992c3d 100644 --- a/non_functional_tests/tests_data/C40_Final.gml +++ b/non_functional_tests/tests_data/C40_Final.gml @@ -10,7 +10,7 @@ -large office +residential 2020 12.822875976562045 2 @@ -135,7 +135,8 @@ -office/workshop +residential +2020 17.09716796875 2 6 @@ -259,7 +260,8 @@ -office/workshop +residential +2020 6.411376953125 1 4.5 @@ -671,7 +673,8 @@ -office/workshop +residential +2020 6.411376953125 1 4.5 @@ -939,7 +942,8 @@ -Corridors/heated +residential +2020 6.411437988281023 1 4.5 @@ -1063,7 +1067,8 @@ -office/workshop +residential +2020 6.411376953125 1 4.5 @@ -1191,7 +1196,8 @@ -office/workshop +residential +2020 14.532531738281023 3 3.4 @@ -1873,7 +1879,8 @@ -office/workshop +residential +2020 7.002624511718977 1 5 @@ -2073,7 +2080,8 @@ -office/workshop +residential +2020 24.220886230468977 5 3.4 @@ -3097,7 +3105,8 @@ -office/workshop +residential +2020 7.002685546875 1 5