import json from flask import request, Response from flask_restful import Resource from pathlib import Path from geomeppy import IDF import os import glob from hub_api.helpers.session_helper import refresh_session import hub_api.helpers.session_helper as sh import helpers.constants as cte import csv class EnergyDemand(Resource): _THERMOSTAT = 'HVACTEMPLATE:THERMOSTAT' _IDEAL_LOAD_AIR_SYSTEM = 'HVACTEMPLATE:ZONE:IDEALLOADSAIRSYSTEM' _SURFACE = 'BUILDINGSURFACE:DETAILED' _WINDOW_SURFACE = 'FENESTRATIONSURFACE:DETAILED' _CONSTRUCTION = 'CONSTRUCTION' _MATERIAL = 'MATERIAL' _MATERIAL_NOMASS = 'MATERIAL:NOMASS' _ROUGHNESS = 'MediumRough' _HOURLY_SCHEDULE = 'SCHEDULE:DAY:HOURLY' _COMPACT_SCHEDULE = 'SCHEDULE:COMPACT' _FILE_SCHEDULE = 'SCHEDULE:FILE' _ZONE = 'ZONE' _LIGHTS = 'LIGHTS' _PEOPLE = 'PEOPLE' _APPLIANCES = 'OTHEREQUIPMENT' _HEATING_COOLING = 'THERMOSTATSETPOINT:DUALSETPOINT' _INFILTRATION = 'ZONEINFILTRATION:DESIGNFLOWRATE' _BUILDING_SURFACE = 'BuildingSurfaceDetailed' _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', cte.GROUND: 'floor', cte.ROOF: 'roof' } idf_usage = { # todo: make an enum for all the usage types cte.RESIDENTIAL: 'residential_building' } idf_type_limits = { cte.ON_OFF: 'on/off', cte.FRACTION: 'Fraction', cte.ANY_NUMBER: 'Any Number', cte.CONTINUOUS: 'Continuous', cte.DISCRETE: 'Discrete', cte.TEMPERATURE: 'Any Number' } 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' } idf_schedule_types = { 'compact': 'Compact', cte.DAY: 'Day', cte.WEEK: 'Week', cte.YEAR: 'Year', 'file': 'File' } idf_schedule_data_type = { 'compact': 'Compact', 'hourly': 'Hourly', 'daily': 'Daily', 'interval': 'Interval', 'list': 'List', } def __init__(self): # this class is mostly hardcoded, as is intended to be used only for Dompark project, # other projects should use the normal idf workflow instead. self._output_path = Path(Path(__file__).parent.parent / 'tmp').resolve() self._data_path = Path(Path(__file__).parent.parent / 'data').resolve() self._city = None self._greenery_percentage = 0 def _set_layers(self, _idf, name, layers, vegetation=None): if vegetation is not None: _kwargs = {'Name': name, 'Outside_Layer': vegetation.name} for i in range(0, len(layers)): _kwargs[f'Layer_{i + 2}'] = layers[i].material.name else: _kwargs = {'Name': name, 'Outside_Layer': layers[0].material.name} for i in range(1, len(layers)): _kwargs[f'Layer_{i + 1}'] = layers[i].material.name _idf.newidfobject(self._CONSTRUCTION, **_kwargs) def _update_constructions(self, _idf, ground, roof, wall, vegetation): for construction in _idf.idfobjects[self._CONSTRUCTION]: if construction.Name == 'Project ground floor': # floor self._set_layers(_idf, 'user_floor', ground) elif construction.Name == 'Dompark Roof': # roof self._set_layers(_idf, 'user_roof', roof) elif construction.Name == 'Dompark Roof Vegetation': # roof self._set_layers(_idf, 'user_roof_vegetation', roof, vegetation) elif construction.Name == 'Dompark Wall': # wall self._set_layers(_idf, 'user_wall', wall) else: continue for surface in _idf.idfobjects[self._SURFACE]: if surface.Construction_Name == 'Project ground floor': # floor surface.Construction_Name = 'user_floor' elif surface.Construction_Name == 'Dompark Wall': # wall surface.Construction_Name = 'user_wall' elif surface.Construction_Name == 'Dompark Roof' or surface.Construction_Name == 'Dompark Roof Vegetation': # roof surface.Construction_Name = 'user_roof' if self._greenery_percentage > 0: if surface.Name in sh.roofs_associated_to_percentage[str(self._greenery_percentage)]: surface.Construction_Name = 'user_roof_vegetation' else: continue for window in _idf.idfobjects[self._WINDOW_SURFACE]: window.Construction_Name = 'window_construction_1' def _add_material(self, _idf, layers): for layer in layers: for material in _idf.idfobjects[self._MATERIAL]: if material.Name == layer.material.name: return for material in _idf.idfobjects[self._MATERIAL_NOMASS]: if material.Name == layer.material.name: return if str(layer.material.no_mass) == 'True': _idf.newidfobject(self._MATERIAL_NOMASS, Name=layer.material.name, Roughness=self._ROUGHNESS, Thermal_Resistance=layer.material.thermal_resistance, Thermal_Absorptance=layer.material.thermal_absorptance, Solar_Absorptance=layer.material.solar_absorptance, Visible_Absorptance=layer.material.visible_absorptance ) else: _idf.newidfobject(self._MATERIAL, Name=layer.material.name, Roughness=self._ROUGHNESS, Thickness=layer.thickness, Conductivity=layer.material.conductivity, Density=layer.material.density, Specific_Heat=layer.material.specific_heat, Thermal_Absorptance=layer.material.thermal_absorptance, Solar_Absorptance=layer.material.solar_absorptance, Visible_Absorptance=layer.material.visible_absorptance ) def _add_vegetation_material(self, _idf, vegetation): for vegetation_material in _idf.idfobjects[self._MATERIAL_ROOFVEGETATION]: if vegetation_material.Name == vegetation.name: return soil = vegetation.soil height = 0 leaf_area_index = 0 leaf_reflectivity = 0 leaf_emissivity = 0 minimal_stomatal_resistance = 0 for plant in vegetation.plants: percentage = float(plant.percentage) / 100 height += percentage * float(plant.height) leaf_area_index += percentage * float(plant.leaf_area_index) leaf_reflectivity += percentage * float(plant.leaf_reflectivity) leaf_emissivity += percentage * float(plant.leaf_emissivity) minimal_stomatal_resistance += percentage * float(plant.minimal_stomatal_resistance) _idf.newidfobject(self._MATERIAL_ROOFVEGETATION, Name=vegetation.name, Height_of_Plants=height, Leaf_Area_Index=leaf_area_index, Leaf_Reflectivity=leaf_reflectivity, Leaf_Emissivity=leaf_emissivity, Minimum_Stomatal_Resistance=minimal_stomatal_resistance, Soil_Layer_Name=soil.name, Roughness=soil.roughness, Thickness=vegetation.soil_thickness, Conductivity_of_Dry_Soil=soil.dry_conductivity, Density_of_Dry_Soil=soil.dry_density, Specific_Heat_of_Dry_Soil=soil.dry_specific_heat, Thermal_Absorptance=soil.thermal_absorptance, Solar_Absorptance=soil.solar_absorptance, Visible_Absorptance=soil.visible_absorptance, Saturation_Volumetric_Moisture_Content_of_the_Soil_Layer= soil.saturation_volumetric_moisture_content, Residual_Volumetric_Moisture_Content_of_the_Soil_Layer= soil.residual_volumetric_moisture_content, Initial_Volumetric_Moisture_Content_of_the_Soil_Layer= soil.initial_volumetric_moisture_content, Moisture_Diffusion_Calculation_Method=self._SIMPLE ) def _add_window_construction_and_material(self, _idf, window): name = 'glazing_1' _kwargs = {'Name': name, 'UFactor': window.overall_u_value, 'Solar_Heat_Gain_Coefficient': window.g_value} _idf.newidfobject(self._WINDOW_MATERIAL_SIMPLE, **_kwargs) window_construction_name = 'window_construction_1' _kwargs = {'Name': window_construction_name, 'Outside_Layer': name} return _idf.newidfobject(self._CONSTRUCTION, **_kwargs) def _add_materials(self, _idf): building = self._city.buildings[0] ground_surface = building.grounds[0] roof_surface = building.roofs[0] wall_surface = building.walls[0] internal_zone = building.internal_zones[0] thermal_zone = internal_zone.thermal_zones[0] ground = None roof = None roof_vegetation = None wall = None window = None for thermal_boundary in thermal_zone.thermal_boundaries: if thermal_boundary.parent_surface.id == wall_surface.id: wall = thermal_boundary.layers if thermal_boundary.parent_surface.id == roof_surface.id: roof = thermal_boundary.layers roof_vegetation = thermal_boundary.vegetation if thermal_boundary.parent_surface.id == ground_surface.id: ground = thermal_boundary.layers if thermal_boundary.thermal_openings is not None and len(thermal_boundary.thermal_openings) > 0: window = thermal_boundary.thermal_openings[0] if ground is not None and roof is not None and wall is not None and window is not None: # we have all the needed surfaces type break self._add_material(_idf, ground) self._add_material(_idf, roof) self._add_material(_idf, wall) if roof_vegetation is not None: self._add_vegetation_material(_idf, roof_vegetation) self._update_constructions(_idf, ground, roof, wall, roof_vegetation) self._add_window_construction_and_material(_idf, window) def _add_standard_compact_hourly_schedule(self, _idf, schedule_name, schedules): _kwargs = {'Name': f'{schedule_name}', 'Schedule_Type_Limits_Name': self.idf_type_limits[schedules[0].data_type], 'Field_1': 'Through: 12/31'} for j, schedule in enumerate(schedules): _val = schedule.values _new_field = '' for day_type in schedule.day_types: _new_field += f' {self.idf_day_types[day_type]}' _kwargs[f'Field_{j * 25 + 2}'] = f'For:{_new_field}' for i in range(0, len(_val)): _kwargs[f'Field_{j * 25 + 3 + i}'] = f'Until: {i + 1:02d}:00,{_val[i]}' _idf.newidfobject(self._COMPACT_SCHEDULE, **_kwargs) def _add_schedules(self, _idf, thermal_zone): self._add_standard_compact_hourly_schedule(_idf, 'user_occupancy_schedule', thermal_zone.occupancy.occupancy_schedules) self._add_standard_compact_hourly_schedule(_idf, 'user_lighting_schedule', thermal_zone.lighting.schedules) self._add_standard_compact_hourly_schedule(_idf, 'user_appliances_schedule', thermal_zone.appliances.schedules) self._add_standard_compact_hourly_schedule(_idf, 'user_heating_schedule', thermal_zone.thermal_control.heating_set_point_schedules) self._add_standard_compact_hourly_schedule(_idf, 'user_cooling_schedule', thermal_zone.thermal_control.cooling_set_point_schedules) def _add_usage(self, _idf): _thermal_zone = None for building in self._city.buildings: for internal_zone in building.internal_zones: for thermal_zone in internal_zone.thermal_zones: _thermal_zone = thermal_zone # Dompark project share schedules and usages among all the buildings so we could add just the first one break break break self._add_schedules(_idf, _thermal_zone) fraction_radiant = _thermal_zone.occupancy.sensible_radiative_internal_gain / \ (_thermal_zone.occupancy.sensible_radiative_internal_gain + _thermal_zone.occupancy.sensible_convective_internal_gain) for idf_object in _idf.idfobjects[self._PEOPLE]: idf_object['Number_of_People_Schedule_Name'] = 'user_occupancy_schedule' idf_object['People_per_Zone_Floor_Area'] = _thermal_zone.occupancy.occupancy_density idf_object['Fraction_Radiant'] = fraction_radiant for idf_object in _idf.idfobjects[self._LIGHTS]: idf_object['Schedule_Name'] = 'user_lighting_schedule' idf_object['Watts_per_Zone_Floor_Area'] = _thermal_zone.lighting.density for idf_object in _idf.idfobjects[self._APPLIANCES]: idf_object['Schedule_Name'] = 'user_appliances_schedule' idf_object['Power_per_Zone_Floor_Area'] = _thermal_zone.appliances.density for idf_object in _idf.idfobjects[self._HEATING_COOLING]: idf_object['Heating_Setpoint_Temperature_Schedule_Name'] = 'user_heating_schedule' idf_object['Cooling_Setpoint_Temperature_Schedule_Name'] = 'user_cooling_schedule' return def get(self): session = refresh_session(request) if session is None: return Response(json.dumps({'error': 'invalid session'}), status=401) headers = session.headers self._city = session.city self._greenery_percentage = round(float(session.greenery_percentage) / 10) * 10 output_file = str((self._output_path / 'dompark.idf').resolve()) idd_file = str((self._data_path / 'energy+.idd').resolve()) epw_file = str((self._data_path / 'CAN_PQ_Montreal.Intl.AP.716270_CWEC.epw').resolve()) idf_file = str((self._data_path / 'dompark.idf').resolve()) IDF.setiddname(idd_file) _idf = IDF(idf_file, epw_file) self._add_materials(_idf) self._add_usage(_idf) _idf.newidfobject( "OUTPUT:VARIABLE", Variable_Name="Zone Ideal Loads Supply Air Total Heating Energy", Reporting_Frequency="Hourly", ) _idf.newidfobject( "OUTPUT:VARIABLE", Variable_Name="Zone Ideal Loads Supply Air Total Cooling Energy", Reporting_Frequency="Hourly", ) # From EnergyPlus documentation: Lights Electric Energy [J] # The lighting electrical consumption including ballasts, if present. These will have the same value as Lights # Total Heating Energy (above). _idf.newidfobject( "OUTPUT:VARIABLE", Variable_Name="Lights Total Heating Energy", Reporting_Frequency="Hourly", ) _idf.newidfobject( "OUTPUT:VARIABLE", Variable_Name="Other Equipment Total Heating Energy", Reporting_Frequency="Hourly", ) _idf.match() _idf.saveas(str(output_file)) _idf.run(expandobjects=True, readvars=True, output_directory=self._output_path, output_prefix='dompark_') # Todo: set the heating and cooling heating = [] cooling = [] lighting = [] appliances = [] with open((self._output_path / f'dompark_out.csv').resolve()) as f: reader = csv.reader(f, delimiter=',') for row in reader: if '00:00' in row[0]: cooling_value = 0.0 heating_value = 0.0 lighting_value = 0.0 appliances_value = 0.0 for i in range(1, 38): lighting_value += float(row[i]) for i in range(38, 73): appliances_value += float(row[i]) for i in range(73, 133, 2): heating_value += float(row[i]) cooling_value += float(row[i + 1]) cooling.append(cooling_value) heating.append(heating_value) lighting.append(lighting_value) appliances.append(appliances_value) files = glob.glob(f'{self._output_path}/dompark*') for file in files: os.remove(file) continue response = {'heating_demand': heating, 'cooling_demand': cooling, 'lighting_demand': lighting, 'appliances_demand': appliances } return Response(json.dumps(response), headers=headers)