api_v1.4/hub_api/energy_demand.py

397 lines
16 KiB
Python

import json
from flask import request, Response
from flask_restful import Resource
from pathlib import Path
from geomeppy import IDF
import os
import glob
import hub_api.helpers.session_helper as sh
import helpers.constants as cte
import csv
from hub_api.helpers.auth import role_required
from persistence.models import UserRoles
from hub_api.config import Config
class EnergyDemand(Resource, Config):
_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.
super().__init__()
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
@role_required([UserRoles.Admin.value, UserRoles.Hub_Reader.value])
def get(self, city_id):
payload = request.get_json()
self._city = self.get_city(city_id)
self._greenery_percentage = round(float(payload['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), status=200)