diff --git a/city_model_structure/building_demand/thermal_zone.py b/city_model_structure/building_demand/thermal_zone.py index ce66223d..a9420840 100644 --- a/city_model_structure/building_demand/thermal_zone.py +++ b/city_model_structure/building_demand/thermal_zone.py @@ -129,10 +129,10 @@ class ThermalZone: self._indirectly_heated_area_ratio = float(value) @property - def infiltration_rate_system_on(self) -> Union[None, Schedule]: + def infiltration_rate_system_on(self): """ Get thermal zone infiltration rate system on in air changes per hour (ACH) - :return: None or Schedule + :return: None or float """ return self._infiltration_rate_system_on @@ -140,15 +140,15 @@ class ThermalZone: def infiltration_rate_system_on(self, value): """ Set thermal zone infiltration rate system on in air changes per hour (ACH) - :param value: Schedule + :param value: float """ self._infiltration_rate_system_on = value @property - def infiltration_rate_system_off(self) -> Union[None, Schedule]: + def infiltration_rate_system_off(self): """ Get thermal zone infiltration rate system off in air changes per hour (ACH) - :return: None or Schedule + :return: None or float """ return self._infiltration_rate_system_off @@ -156,7 +156,7 @@ class ThermalZone: def infiltration_rate_system_off(self, value): """ Set thermal zone infiltration rate system on in air changes per hour (ACH) - :param value: Schedule + :param value: float """ self._infiltration_rate_system_off = value diff --git a/exports/formats/idf.py b/exports/formats/idf.py index 69722d72..6a56ac0b 100644 --- a/exports/formats/idf.py +++ b/exports/formats/idf.py @@ -1,20 +1,22 @@ """ -TestOccupancyFactory test and validate the city model structure schedules parameters +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 Soroush Samareh Abolhassani soroush.samarehabolhassani@mail.concordia.ca """ - +import copy +import math from pathlib import Path from geomeppy import IDF import helpers.constants as cte +from city_model_structure.attributes.schedule import Schedule class Idf: """ - Export city to IDF + Exports city to IDF """ _THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT' _IDEAL_LOAD_AIR_SYSTEM = 'HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM' @@ -29,7 +31,6 @@ class Idf: _ZONE = 'ZONE' _LIGHTS = 'LIGHTS' _PEOPLE = 'PEOPLE' - _ELECTRIC_EQUIPMENT = 'ELECTRICEQUIPMENT' _INFILTRATION = 'ZONEINFILTRATION:DESIGNFLOWRATE' _BUILDING_SURFACE = 'BuildingSurfaceDetailed' _SCHEDULE_LIMIT = 'SCHEDULETYPELIMITS' @@ -38,6 +39,11 @@ class Idf: _ANY_NUMBER = 'Any Number' _CONTINUOUS = 'Continuous' _DISCRETE = 'Discrete' + _BUILDING = 'BUILDING' + _SIZING_PERIODS = 'SIZINGPERIOD:DESIGNDAY' + _LOCATION = 'SITE:LOCATION' + _WINDOW_MATERIAL_SIMPLE = 'WINDOWMATERIAL:SIMPLEGLAZINGSYSTEM' + _WINDOW = 'WINDOW' idf_surfaces = { # todo: make an enum for all the surface types @@ -162,9 +168,6 @@ class Idf: _kwargs[f'Field_{j * 25 + 3 + i}'] = f'Until: {i + 1:02d}:00,{_val[i]}' self._idf.newidfobject(self._COMPACT_SCHEDULE, **_kwargs) - def _add_non_hourly_schedule(self, usage, schedules): - raise NotImplementedError - def _write_schedules_file(self, usage, schedule): file_name = str((Path(self._output_path) / f'{schedule.type} schedules {usage}.dat').resolve()) with open(file_name, 'w') as file: @@ -183,6 +186,28 @@ class Idf: _schedule.Interpolate_to_Timestep = 'No' _schedule.Minutes_per_Item = 60 + def _add_infiltration_schedules(self, thermal_zone): + _infiltration_schedules = [] + 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(thermal_zone.infiltration_rate_system_off) + else: + _infiltration_values.append(thermal_zone.infiltration_rate_system_on) + _schedule.values = _infiltration_values + _infiltration_schedules.append(_schedule) + for schedule in self._idf.idfobjects[self._HOURLY_SCHEDULE]: + if schedule.Name == f'{_infiltration_schedules[0].type} schedules {thermal_zone.usage}': + return + return self._add_standard_compact_hourly_schedule(thermal_zone.usage, _infiltration_schedules) + def _add_schedules(self, usage, new_schedules, schedule_from_file=False): if schedule_from_file: new_schedule = new_schedules[0] @@ -217,6 +242,22 @@ class Idf: _kwargs[f'Layer_{i + 1}'] = layers[1].material.name self._idf.newidfobject(self._CONSTRUCTION, **_kwargs) + def _add_window_construction_and_material(self, thermal_opening): + for window_material in self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]: + if window_material['UFactor'] == thermal_opening.overall_u_value and \ + window_material['Solar_Heat_Gain_Coefficient'] == thermal_opening.g_value: + return + + order = str(len(self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]) + 1) + material_name = 'glazing_' + order + _kwargs = {'Name': material_name, 'UFactor': thermal_opening.overall_u_value, + 'Solar_Heat_Gain_Coefficient': thermal_opening.g_value} + self._idf.newidfobject(self._WINDOW_MATERIAL_SIMPLE, **_kwargs) + + window_construction_name = 'window_construction_' + order + _kwargs = {'Name': window_construction_name, 'Outside_Layer': material_name} + self._idf.newidfobject(self._CONSTRUCTION, **_kwargs) + def _add_zone(self, thermal_zone): for zone in self._idf.idfobjects['ZONE']: if zone.Name == thermal_zone.id: @@ -263,52 +304,56 @@ class Idf: Activity_Level_Schedule_Name=f'Occupancy schedules {thermal_zone.usage}' ) - 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, usage_zone): + def _add_infiltration(self, thermal_zone): for zone in self._idf.idfobjects["ZONE"]: - if zone.Name == f'{usage_zone.id}_infiltration': + if zone.Name == f'{thermal_zone.id}_infiltration': return 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}', + Name=f'{thermal_zone.id}_infiltration', + Zone_or_ZoneList_Name=thermal_zone.id, + Schedule_Name=f'Infiltration schedules {thermal_zone.usage}', Design_Flow_Rate_Calculation_Method='AirChanges/Hour', - Air_Changes_per_Hour=usage_zone.mechanical_air_change, - 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 + Air_Changes_per_Hour=thermal_zone.mechanical_air_change ) + def _rename_building(self, b): + for building in self._idf.idfobjects[self._BUILDING]: + building.Name = b.name + building['Solar_Distribution'] = 'FullExterior' + + def _remove_sizing_periods(self): + while len(self._idf.idfobjects[self._SIZING_PERIODS]) > 0: + self._idf.popidfobject(self._SIZING_PERIODS, 0) + + def _remove_location(self): + self._idf.popidfobject(self._LOCATION, 0) + def _export(self): """ Export the idf file into the given path export type = "Surfaces|Block" """ + self._remove_location() + self._remove_sizing_periods() 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: self._add_construction(thermal_boundary) + for thermal_opening in thermal_boundary.thermal_openings: + self._add_window_construction_and_material(thermal_opening) usage = thermal_zone.usage - # todo: infiltration can be written with two values (system on and system off) in E+? Or just as schedule? - # self._add_schedule(usage, "Infiltration") + self._add_infiltration_schedules(thermal_zone) + # todo: why is this schedule unused? self._add_schedules(usage, thermal_zone.lighting.schedules) self._add_schedules(usage, thermal_zone.occupancy.occupancy_schedules, schedule_from_file=True) self._add_schedules(usage, thermal_zone.thermal_control.hvac_availability_schedules) self._add_zone(thermal_zone) self._add_heating_system(thermal_zone) -# self._add_infiltration(usage_zone) + self._add_infiltration(thermal_zone) self._add_occupancy(thermal_zone) if self._export_type == "Surfaces": @@ -355,14 +400,37 @@ class Idf: self._idf.intersect_match() def _add_surfaces(self, building): - for internal_zone in building.internal_zones: for thermal_zone in internal_zone.thermal_zones: for boundary in thermal_zone.thermal_boundaries: idf_surface_type = self.idf_surfaces[boundary.parent_surface.type] + # todo: thermal boundary vs. surfaces?? surface = self._idf.newidfobject(self._SURFACE, Name=f'{boundary.parent_surface.name}', Surface_Type=idf_surface_type, Zone_Name=thermal_zone.id, Construction_Name=boundary.construction_name) coordinates = self._matrix_to_list(boundary.parent_surface.solid_polygon.coordinates, self._city.lower_corner) surface.setcoords(coordinates) + self._add_windows(boundary) + + def _add_windows(self, boundary): + for opening in boundary.thermal_openings: + for construction in self._idf.idfobjects[self._CONSTRUCTION]: + if construction['Outside_Layer'].split('_')[0] == 'glazing': + window_construction = construction + if self._compare_window_constructions(window_construction, opening): + opening_name = 'window_' + str(len(self._idf.idfobjects[self._WINDOW]) + 1) + opening_length = math.sqrt(opening.area) + self._idf.newidfobject(self._WINDOW, Name=f'{opening_name}', Construction_Name=window_construction['Name'], + Building_Surface_Name=boundary.parent_surface.name, Multiplier='1', + Length=opening_length, Height=opening_length) + print('bb') + + def _compare_window_constructions(self, window_construction, opening): + glazing = window_construction['Outside_Layer'] + for material in self._idf.idfobjects[self._WINDOW_MATERIAL_SIMPLE]: + if material['Name'] == glazing: + if material['UFactor'] == opening.overall_u_value and \ + material['Solar_Heat_Gain_Coefficient'] == opening.g_value: + return True + return False diff --git a/imports/construction/us_physics_parameters.py b/imports/construction/us_physics_parameters.py index 2b06eeb7..00f4a8fa 100644 --- a/imports/construction/us_physics_parameters.py +++ b/imports/construction/us_physics_parameters.py @@ -65,10 +65,7 @@ class UsPhysicsParameters(NrelPhysicsInterface): @staticmethod def _search_archetype(function, year_of_construction, climate_zone): nrel_catalog = ConstructionCatalogFactory('nrel').catalog - print(nrel_catalog.names) nrel_archetypes = nrel_catalog.entries('archetypes') - for nrel_archetype in nrel_archetypes: - print(nrel_archetype.name) for building_archetype in nrel_archetypes: construction_period_limits = building_archetype.construction_period.split(' - ') if construction_period_limits[1] == 'PRESENT':