397 lines
16 KiB
Python
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)
|