city_retrofit/hub/city_model_structure/building.py

686 lines
22 KiB
Python
Raw Normal View History

"""
Building module
SPDX - License - Identifier: LGPL - 3.0 - or -later
Copyright © 2022 Concordia CERC group
Project Coder Guille Gutierrez guillermo.gutierrezmorote@concordia.ca
Code contributors: Pilar Monsalvete Alvarez de Uribarri pilar.monsalvete@concordia.ca
"""
2023-02-09 05:29:53 -05:00
import sys
from typing import List, Union
import numpy as np
2023-04-27 13:31:00 -04:00
import pandas as pd
2023-02-09 05:29:53 -05:00
from hub.hub_logger import logger
2023-01-24 10:51:50 -05:00
import hub.helpers.constants as cte
2023-05-12 09:27:29 -04:00
from hub.helpers.peak_loads import PeakLoads
2023-01-24 10:51:50 -05:00
from hub.city_model_structure.building_demand.surface import Surface
from hub.city_model_structure.city_object import CityObject
from hub.city_model_structure.building_demand.household import Household
from hub.city_model_structure.building_demand.internal_zone import InternalZone
from hub.city_model_structure.attributes.polyhedron import Polyhedron
from hub.city_model_structure.energy_systems.energy_system import EnergySystem
2021-03-31 14:17:53 -04:00
class Building(CityObject):
"""
Building(CityObject) class
"""
2022-11-28 13:44:02 -05:00
def __init__(self, name, surfaces, year_of_construction, function, terrains=None):
super().__init__(name, surfaces)
2021-10-18 16:07:18 -04:00
self._households = None
self._basement_heated = None
self._attic_heated = None
self._terrains = terrains
self._year_of_construction = year_of_construction
self._function = function
self._average_storey_height = None
self._storeys_above_ground = None
self._floor_area = None
self._roof_type = None
2022-03-08 20:08:03 -05:00
self._internal_zones = None
self._shell = None
2023-01-31 13:11:39 -05:00
self._alias = None
self._type = 'building'
2023-03-20 14:15:57 -04:00
self._cold_water_temperature = dict()
self._heating = dict()
self._cooling = dict()
self._lighting_electrical_demand = dict()
self._appliances_electrical_demand = dict()
self._domestic_hot_water_heat_demand = dict()
self._heating_consumption = dict()
self._cooling_consumption = dict()
self._domestic_hot_water_consumption = dict()
self._distribution_systems_electrical_consumption = dict()
self._onsite_electrical_production = dict()
2021-03-31 14:17:53 -04:00
self._eave_height = None
self._energy_systems = None
self._systems_archetype_name = None
self._grounds = []
self._roofs = []
self._walls = []
self._internal_walls = []
2023-02-09 05:29:53 -05:00
self._ground_walls = []
self._attic_floors = []
self._interior_slabs = []
for surface_id, surface in enumerate(self.surfaces):
self._min_x = min(self._min_x, surface.lower_corner[0])
self._min_y = min(self._min_y, surface.lower_corner[1])
self._min_z = min(self._min_z, surface.lower_corner[2])
surface.id = surface_id
if surface.type == cte.GROUND:
self._grounds.append(surface)
elif surface.type == cte.WALL:
self._walls.append(surface)
elif surface.type == cte.ROOF:
self._roofs.append(surface)
2023-02-09 05:29:53 -05:00
elif surface.type == cte.INTERIOR_WALL:
self._internal_walls.append(surface)
2023-02-09 05:29:53 -05:00
elif surface.type == cte.GROUND_WALL:
self._ground_walls.append(surface)
elif surface.type == cte.ATTIC_FLOOR:
self._attic_floors.append(surface)
elif surface.type == cte.INTERIOR_SLAB:
self._interior_slabs.append(surface)
else:
logger.error(f'Building {self.name} [alias {self.alias}] has an unexpected surface type {surface.type}.\n')
sys.stderr.write(f'Building {self.name} [alias {self.alias}] has an unexpected surface type {surface.type}.\n')
@property
2022-03-08 20:08:03 -05:00
def shell(self) -> Polyhedron:
"""
Get building's external polyhedron
2022-03-08 20:08:03 -05:00
:return: [Polyhedron]
"""
polygons = []
for surface in self.surfaces:
if surface.type is not cte.INTERIOR_WALL:
polygons.append(surface.solid_polygon)
if surface.holes_polygons is not None:
for hole in surface.holes_polygons:
polygons.append(hole)
2022-03-08 20:08:03 -05:00
if self._shell is None:
self._shell = Polyhedron(polygons)
2022-03-08 20:08:03 -05:00
return self._shell
@property
def internal_zones(self) -> List[InternalZone]:
"""
Get building internal zones
For Lod up to 3, there is only one internal zone which corresponds to the building shell.
In LoD 4 there can be more than one. In this case the definition of surfaces and floor area must be redefined.
:return: [InternalZone]
"""
if self._internal_zones is None:
self._internal_zones = [InternalZone(self.surfaces, self.floor_area)]
return self._internal_zones
@property
2021-09-01 09:39:27 -04:00
def grounds(self) -> List[Surface]:
"""
Get building ground surfaces
2021-09-01 09:39:27 -04:00
:return: [Surface]
"""
return self._grounds
@property
2021-09-01 09:39:27 -04:00
def roofs(self) -> List[Surface]:
"""
Get building roof surfaces
:return: [Surface]
"""
return self._roofs
@property
2021-09-01 09:39:27 -04:00
def walls(self) -> List[Surface]:
"""
Get building wall surfaces
:return: [Surface]
"""
return self._walls
@property
def internal_walls(self) -> List[Surface]:
"""
Get building internal wall surfaces
:return: [Surface]
"""
return self._internal_walls
@property
2022-03-08 19:19:52 -05:00
def terrains(self) -> Union[None, List[Surface]]:
"""
Get city object terrain surfaces
:return: [Surface]
"""
return self._terrains
@property
2021-09-14 13:46:48 -04:00
def attic_heated(self) -> Union[None, int]:
"""
Get if the city object attic is heated
2022-11-25 14:45:03 -05:00
0: no attic in the building
1: attic exists but is not heated
2: attic exists and is heated
2021-09-14 13:46:48 -04:00
:return: None or int
"""
return self._attic_heated
@attic_heated.setter
def attic_heated(self, value):
"""
Set if the city object attic is heated
2022-11-25 14:45:03 -05:00
0: no attic in the building
1: attic exists but is not heated
2: attic exists and is heated
2021-09-14 13:46:48 -04:00
:param value: int
"""
2021-09-14 13:46:48 -04:00
if value is not None:
self._attic_heated = int(value)
@property
2021-09-14 13:46:48 -04:00
def basement_heated(self) -> Union[None, int]:
"""
Get if the city object basement is heated
2022-11-25 14:45:03 -05:00
0: no basement in the building
1: basement exists but is not heated
2: basement exists and is heated
2021-09-14 13:46:48 -04:00
:return: None or int
"""
return self._basement_heated
@basement_heated.setter
def basement_heated(self, value):
"""
Set if the city object basement is heated
2022-11-25 14:45:03 -05:00
0: no basement in the building
1: basement exists but is not heated
2: basement exists and is heated
2021-09-14 13:46:48 -04:00
:param value: int
"""
2021-09-14 13:46:48 -04:00
if value is not None:
self._basement_heated = int(value)
@property
def year_of_construction(self):
"""
Get building year of construction
:return: int
"""
return self._year_of_construction
@year_of_construction.setter
def year_of_construction(self, value):
"""
Set building year of construction
:param value: int
"""
if value is not None:
2022-03-08 19:19:52 -05:00
self._year_of_construction = int(value)
@property
2021-09-14 13:46:48 -04:00
def function(self) -> Union[None, str]:
"""
Get building function
2021-09-14 13:46:48 -04:00
:return: None or str
"""
return self._function
@function.setter
def function(self, value):
"""
Set building function
:param value: str
"""
2021-09-14 13:46:48 -04:00
if value is not None:
self._function = str(value)
@property
2021-09-14 13:46:48 -04:00
def average_storey_height(self) -> Union[None, float]:
"""
Get building average storey height in meters
2021-09-14 13:46:48 -04:00
:return: None or float
"""
return self._average_storey_height
@average_storey_height.setter
def average_storey_height(self, value):
"""
Set building average storey height in meters
:param value: float
"""
2021-09-14 13:46:48 -04:00
if value is not None:
self._average_storey_height = float(value)
@property
2021-09-14 13:46:48 -04:00
def storeys_above_ground(self) -> Union[None, int]:
"""
Get building storeys number above ground
2021-09-14 13:46:48 -04:00
:return: None or int
"""
return self._storeys_above_ground
@storeys_above_ground.setter
def storeys_above_ground(self, value):
"""
Set building storeys number above ground
:param value: int
"""
2021-09-14 13:46:48 -04:00
if value is not None:
self._storeys_above_ground = int(value)
2023-03-20 14:15:57 -04:00
@property
def cold_water_temperature(self) -> {float}:
"""
Get cold water temperature in degrees Celsius
:return: dict{DataFrame(float)}
"""
return self._cold_water_temperature
@cold_water_temperature.setter
def cold_water_temperature(self, value):
"""
Set cold water temperature in degrees Celsius
:param value: dict{DataFrame(float)}
"""
self._cold_water_temperature = value
@property
def heating(self) -> dict:
"""
Get heating demand in Wh
:return: dict{DataFrame(float)}
"""
return self._heating
@heating.setter
def heating(self, value):
"""
Set heating demand in Wh
:param value: dict{DataFrame(float)}
"""
self._heating = value
@property
def cooling(self) -> dict:
"""
Get cooling demand in Wh
:return: dict{DataFrame(float)}
"""
return self._cooling
@cooling.setter
def cooling(self, value):
"""
Set cooling demand in Wh
:param value: dict{DataFrame(float)}
"""
self._cooling = value
@property
def lighting_electrical_demand(self) -> dict:
"""
Get lighting electrical demand in Wh
:return: dict{DataFrame(float)}
"""
return self._lighting_electrical_demand
@lighting_electrical_demand.setter
def lighting_electrical_demand(self, value):
"""
Set lighting electrical demand in Wh
:param value: dict{DataFrame(float)}
"""
self._lighting_electrical_demand = value
@property
def appliances_electrical_demand(self) -> dict:
"""
Get appliances electrical demand in Wh
:return: dict{DataFrame(float)}
"""
return self._appliances_electrical_demand
@appliances_electrical_demand.setter
def appliances_electrical_demand(self, value):
"""
Set appliances electrical demand in Wh
:param value: dict{DataFrame(float)}
"""
self._appliances_electrical_demand = value
@property
def domestic_hot_water_heat_demand(self) -> dict:
"""
Get domestic hot water heat demand in Wh
:return: dict{DataFrame(float)}
"""
return self._domestic_hot_water_heat_demand
@domestic_hot_water_heat_demand.setter
def domestic_hot_water_heat_demand(self, value):
"""
Set domestic hot water heat demand in Wh
:param value: dict{DataFrame(float)}
"""
self._domestic_hot_water_heat_demand = value
2023-04-18 12:21:00 -04:00
@property
2023-05-12 09:27:29 -04:00
def heating_peak_load(self) -> Union[None, dict]:
2023-04-18 12:21:00 -04:00
"""
Get heating peak load in W
:return: dict{DataFrame(float)}
"""
2023-04-27 13:31:00 -04:00
results = {}
2023-05-01 10:37:51 -04:00
if cte.HOUR in self.heating:
2023-05-12 09:27:29 -04:00
monthly_values = PeakLoads().\
peak_loads_from_hourly(self.heating[cte.HOUR][next(iter(self.heating[cte.HOUR]))].values)
2023-05-01 10:37:51 -04:00
else:
2023-05-12 09:27:29 -04:00
monthly_values = PeakLoads(self).heating_peak_loads_from_methodology
if monthly_values is None:
return None
2023-05-01 10:37:51 -04:00
results[cte.MONTH] = pd.DataFrame(monthly_values, columns=['heating peak loads'])
results[cte.YEAR] = pd.DataFrame([max(monthly_values)], columns=['heating peak loads'])
2023-04-27 13:31:00 -04:00
return results
2023-04-18 12:21:00 -04:00
@property
2023-05-12 09:27:29 -04:00
def cooling_peak_load(self) -> Union[None, dict]:
2023-04-18 12:21:00 -04:00
"""
Get cooling peak load in W
:return: dict{DataFrame(float)}
"""
2023-04-27 13:31:00 -04:00
results = {}
2023-05-12 09:27:29 -04:00
monthly_values = None
2023-05-01 10:37:51 -04:00
if cte.HOUR in self.cooling:
2023-05-12 09:27:29 -04:00
# todo: .values???????? Like heating
monthly_values = PeakLoads().peak_loads_from_hourly(self.cooling[cte.HOUR][next(iter(self.cooling[cte.HOUR]))])
2023-05-01 10:37:51 -04:00
else:
2023-05-12 09:27:29 -04:00
monthly_values = PeakLoads(self).cooling_peak_loads_from_methodology
if monthly_values is None:
return None
2023-05-01 10:37:51 -04:00
results[cte.MONTH] = pd.DataFrame(monthly_values, columns=['cooling peak loads'])
results[cte.YEAR] = pd.DataFrame([max(monthly_values)], columns=['cooling peak loads'])
return results
2023-04-18 12:21:00 -04:00
@property
2021-03-31 14:17:53 -04:00
def eave_height(self):
"""
Get building eave height in meters
2021-03-31 14:17:53 -04:00
:return: float
"""
if self._eave_height is None:
self._eave_height = 0
for wall in self.walls:
self._eave_height = max(self._eave_height, wall.upper_corner[2])
2021-03-31 14:17:53 -04:00
return self._eave_height
@property
def roof_type(self):
"""
Get roof type for the building flat or pitch
:return: str
"""
if self._roof_type is None:
self._roof_type = 'flat'
for roof in self.roofs:
grads = np.rad2deg(roof.inclination)
if 355 > grads > 5:
self._roof_type = 'pitch'
break
return self._roof_type
2022-03-08 19:19:52 -05:00
@roof_type.setter
def roof_type(self, value):
"""
Set roof type for the building flat or pitch
:return: str
"""
self._roof_type = value
@property
def floor_area(self):
"""
Get building floor area in square meters
:return: float
"""
if self._floor_area is None:
self._floor_area = 0
for surface in self.surfaces:
if surface.type == 'Ground':
self._floor_area += surface.perimeter_polygon.area
return self._floor_area
2021-10-18 16:07:18 -04:00
@property
def households(self) -> List[Household]:
"""
Get the list of households inside the building
:return: List[Household]
"""
return self._households
2022-03-08 19:19:52 -05:00
@property
def is_conditioned(self):
"""
Get building heated flag
:return: Boolean
"""
if self.internal_zones is None:
2022-03-08 19:19:52 -05:00
return False
for internal_zone in self.internal_zones:
2022-12-06 15:28:59 -05:00
if internal_zone.usages is not None:
2023-01-26 08:53:00 -05:00
for usage in internal_zone.usages:
if usage.thermal_control is not None:
return True
2022-03-08 19:19:52 -05:00
return False
@property
2023-01-31 13:11:39 -05:00
def alias(self):
"""
2023-01-31 13:11:39 -05:00
Get the alias name for the building
:return: str
"""
2023-01-31 13:11:39 -05:00
return self._alias
2023-01-31 13:11:39 -05:00
@alias.setter
def alias(self, value):
"""
2023-01-31 13:11:39 -05:00
Set the alias name for the building
"""
2023-01-31 13:11:39 -05:00
self._alias = value
2023-02-07 06:16:08 -05:00
@property
def usages_percentage(self):
"""
Get the usages and percentages for the building
"""
_usage = ''
for internal_zone in self.internal_zones:
for usage in internal_zone.usages:
_usage = f'{_usage}{usage.name}_{usage.percentage} '
return _usage.rstrip()
@property
def energy_systems(self) -> Union[None, List[EnergySystem]]:
"""
Get list of energy systems installed to cover the building demands
:return: [EnergySystem]
"""
return self._energy_systems
@energy_systems.setter
def energy_systems(self, value):
"""
Set list of energy systems installed to cover the building demands
:param value: [EnergySystem]
"""
self._energy_systems = value
@property
2023-05-02 12:35:42 -04:00
def energy_systems_archetype_name(self):
"""
2023-05-02 12:35:42 -04:00
Get energy systems archetype name
:return: str
"""
return self._systems_archetype_name
2023-05-02 12:35:42 -04:00
@energy_systems_archetype_name.setter
def energy_systems_archetype_name(self, value):
"""
2023-05-02 12:35:42 -04:00
Set energy systems archetype name
:param value: str
"""
self._systems_archetype_name = value
@property
def heating_consumption(self):
"""
Get energy consumption for heating according to the heating system installed in Wh
return: dict
"""
if len(self._heating_consumption) == 0:
for heating_demand_key in self.heating:
demand = self.heating[heating_demand_key][cte.INSEL_MEB]
consumption_type = cte.HEATING
final_energy_consumed = self._calculate_consumption(consumption_type, demand)
self._heating_consumption[heating_demand_key] = final_energy_consumed
return self._heating_consumption
@property
def cooling_consumption(self):
"""
Get energy consumption for cooling according to the cooling system installed in Wh
return: dict
"""
if len(self._cooling_consumption) == 0:
for cooling_demand_key in self.cooling:
demand = self.cooling[cooling_demand_key][cte.INSEL_MEB]
consumption_type = cte.COOLING
final_energy_consumed = self._calculate_consumption(consumption_type, demand)
self._cooling_consumption[cooling_demand_key] = final_energy_consumed
return self._cooling_consumption
@property
def domestic_hot_water_consumption(self):
"""
Get energy consumption for domestic according to the domestic hot water system installed in Wh
return: dict
"""
if len(self._domestic_hot_water_consumption) == 0:
for domestic_hot_water_demand_key in self.domestic_hot_water_heat_demand:
demand = self.domestic_hot_water_heat_demand[domestic_hot_water_demand_key][cte.INSEL_MEB]
consumption_type = cte.DOMESTIC_HOT_WATER
final_energy_consumed = self._calculate_consumption(consumption_type, demand)
self._domestic_hot_water_consumption[domestic_hot_water_demand_key] = final_energy_consumed
return self._domestic_hot_water_consumption
def _calculate_working_hours(self):
_working_hours = {}
for internal_zone in self.internal_zones:
for thermal_zone in internal_zone.thermal_zones:
_working_hours_per_thermal_zone = {}
for schedule in thermal_zone.thermal_control.hvac_availability_schedules:
_working_hours_per_schedule = [0] * len(schedule.values)
for i, value in enumerate(schedule.values):
if value > 0:
_working_hours_per_schedule[i] = 1
for day_type in schedule.day_types:
_working_hours_per_thermal_zone[day_type] = _working_hours_per_schedule
if len(_working_hours) == 0:
_working_hours = _working_hours_per_thermal_zone
else:
for key in _working_hours.keys():
saved_values = _working_hours_per_thermal_zone[key]
for i, value in enumerate(_working_hours[key]):
if saved_values[i] == 1:
_working_hours[key][i] = 1
_total_hours = 0
for key in _working_hours:
_total_hours += _working_hours[key] * cte.DAYS_A_YEAR[key]
return _total_hours
@property
def distribution_systems_electrical_consumption(self):
"""
Get total electricity consumption for distribution and emission systems in Wh
return: dict
"""
if len(self._distribution_systems_electrical_consumption) == 0:
_peak_load = self.heating_peak_load[cte.YEAR]['heating peak loads'][0]
_peak_load_type = cte.HEATING
if _peak_load < self.cooling_peak_load[cte.YEAR]['cooling peak loads'][0]:
_peak_load = self.cooling_peak_load[cte.YEAR]['cooling peak loads'][0]
_peak_load_type = cte.COOLING
_consumption_fix_flow = 0
for energy_system in self.energy_systems:
emission_system = energy_system.emission_system.generic_emission_system
parasitic_energy_consumption = emission_system.parasitic_energy_consumption
distribution_system = energy_system.distribution_system.generic_distribution_system
consumption_variable_flow = distribution_system.distribution_consumption_variable_flow
for demand_type in energy_system.demand_types:
if demand_type.lower() == cte.HEATING:
if _peak_load_type == cte.HEATING:
_consumption_fix_flow = distribution_system.distribution_consumption_fix_flow
for heating_demand_key in self.heating:
_consumption = [0]*len(self.heating)
_demand = self.heating[heating_demand_key][cte.INSEL_MEB]
for i in range(0, len(_consumption)):
_consumption[i] += (parasitic_energy_consumption + consumption_variable_flow) * _demand[i]
self._distribution_systems_electrical_consumption[heating_demand_key] = _consumption
if demand_type.lower() == cte.COOLING:
if _peak_load_type == cte.COOLING:
_consumption_fix_flow = distribution_system.distribution_consumption_fix_flow
for demand_key in self.cooling:
_consumption = self._distribution_systems_electrical_consumption[demand_key]
_demand = self.cooling[demand_key][cte.INSEL_MEB]
for i in range(0, len(_consumption)):
_consumption[i] += (parasitic_energy_consumption + consumption_variable_flow) * _demand[i]
self._distribution_systems_electrical_consumption[demand_key] = _consumption
for key in self._distribution_systems_electrical_consumption:
for i in range(0, len(self._distribution_systems_electrical_consumption[key])):
self._distribution_systems_electrical_consumption[key][i] += _peak_load * _consumption_fix_flow \
* self._calculate_working_hours()
return self._distribution_systems_electrical_consumption
def _calculate_consumption(self, consumption_type, demand):
# todo: modify when COP depends on the hour
coefficient_of_performance = 0
for energy_system in self.energy_systems:
for demand_type in energy_system.demand_types:
2023-05-12 09:27:29 -04:00
if demand_type.lower() == consumption_type.lower():
if consumption_type == cte.HEATING or consumption_type == cte.DOMESTIC_HOT_WATER:
coefficient_of_performance = energy_system.generation_system.generic_generation_system.heat_efficiency
elif consumption_type == cte.COOLING:
coefficient_of_performance = energy_system.generation_system.generic_generation_system.cooling_efficiency
elif consumption_type == cte.ELECTRICITY:
coefficient_of_performance = \
energy_system.generation_system.generic_generation_system.electricity_efficiency
if coefficient_of_performance == 0:
values = [0]*len(demand)
final_energy_consumed = values
else:
final_energy_consumed = []
for demand_value in demand:
final_energy_consumed.append(demand_value / coefficient_of_performance)
return final_energy_consumed
@property
def onsite_electrical_production(self):
"""
Get total electricity produced onsite in Wh
return: dict
"""
# Add other systems whenever new ones appear
for energy_system in self.energy_systems:
if energy_system.generation_system.generic_generation_system.type == cte.PHOTOVOLTAIC:
_efficiency = energy_system.generation_system.generic_generation_system.electricity_efficiency
self._onsite_electrical_production = {}
2023-05-12 09:27:29 -04:00
for _key in self.roofs[0].global_irradiance.keys():
_results = [0 for _ in range(0, len(self.roofs[0].global_irradiance[_key]))]
for surface in self.surfaces:
2023-05-12 09:27:29 -04:00
_results = [x + y * _efficiency * surface.perimeter_area * surface.solar_collectors_area_reduction_factor
for x, y in zip(_results, surface.global_irradiance[_key])]
self._onsite_electrical_production[_key] = _results
return self._onsite_electrical_production