""" ThermalZone 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 """ import uuid import sys import copy from typing import List, Union, TypeVar from city_model_structure.building_demand.occupancy import Occupancy from city_model_structure.building_demand.appliances import Appliances from city_model_structure.building_demand.lighting import Lighting from city_model_structure.building_demand.internal_gain import InternalGain from city_model_structure.building_demand.thermal_control import ThermalControl from city_model_structure.energy_systems.hvac_system import HvacSystem import helpers.constants as cte ThermalBoundary = TypeVar('ThermalBoundary') InternalZone = TypeVar('InternalZone') class ThermalZone: """ ThermalZone class """ def __init__(self, thermal_boundaries, parent_internal_zone, volume, footprint_area): self._id = None self._parent_internal_zone = parent_internal_zone self._footprint_area = footprint_area self._thermal_boundaries = thermal_boundaries self._additional_thermal_bridge_u_value = None self._effective_thermal_capacity = None self._indirectly_heated_area_ratio = None self._infiltration_rate_system_on = None self._infiltration_rate_system_off = None self._volume = volume self._ordinate_number = None self._view_factors_matrix = None self._total_floor_area = None self._usage = None self._not_detailed_source_mean_annual_internal_gains = None self._hours_day = None self._days_year = None self._mechanical_air_change = None self._occupancy = None self._lighting = None self._appliances = None self._internal_gains = None self._thermal_control = None @property def id(self): """ Get thermal zone id, a universally unique identifier randomly generated :return: str """ if self._id is None: self._id = uuid.uuid4() return self._id @property def footprint_area(self) -> float: """ Get thermal zone footprint area in m2 :return: float """ return self._footprint_area @property def thermal_boundaries(self) -> [ThermalBoundary]: """ Get thermal boundaries bounding with the thermal zone :return: [ThermalBoundary] """ return self._thermal_boundaries @property def additional_thermal_bridge_u_value(self) -> Union[None, float]: """ Get thermal zone additional thermal bridge u value W/m2K :return: None or float """ return self._additional_thermal_bridge_u_value @additional_thermal_bridge_u_value.setter def additional_thermal_bridge_u_value(self, value): """ Set thermal zone additional thermal bridge u value W/m2K :param value: float """ if value is not None: self._additional_thermal_bridge_u_value = float(value) @property def effective_thermal_capacity(self) -> Union[None, float]: """ Get thermal zone effective thermal capacity in J/m2K :return: None or float """ return self._effective_thermal_capacity @effective_thermal_capacity.setter def effective_thermal_capacity(self, value): """ Set thermal zone effective thermal capacity in J/m2K :param value: float """ if value is not None: self._effective_thermal_capacity = float(value) @property def indirectly_heated_area_ratio(self) -> Union[None, float]: """ Get thermal zone indirectly heated area ratio :return: None or float """ return self._indirectly_heated_area_ratio @indirectly_heated_area_ratio.setter def indirectly_heated_area_ratio(self, value): """ Set thermal zone indirectly heated area ratio :param value: float """ if value is not None: self._indirectly_heated_area_ratio = float(value) @property def infiltration_rate_system_on(self): """ Get thermal zone infiltration rate system on in air changes per hour (ACH) :return: None or float """ return self._infiltration_rate_system_on @infiltration_rate_system_on.setter def infiltration_rate_system_on(self, value): """ Set thermal zone infiltration rate system on in air changes per hour (ACH) :param value: float """ self._infiltration_rate_system_on = value @property def infiltration_rate_system_off(self): """ Get thermal zone infiltration rate system off in air changes per hour (ACH) :return: None or float """ return self._infiltration_rate_system_off @infiltration_rate_system_off.setter def infiltration_rate_system_off(self, value): """ Set thermal zone infiltration rate system on in air changes per hour (ACH) :param value: float """ self._infiltration_rate_system_off = value @property def volume(self): """ Get thermal zone volume :return: float """ return self._volume @property def ordinate_number(self) -> Union[None, int]: """ Get the order in which the thermal_zones need to be enumerated :return: None or int """ return self._ordinate_number @ordinate_number.setter def ordinate_number(self, value): """ Set a specific order of the zones to be called :param value: int """ if value is not None: self._ordinate_number = int(value) @property def hvac_system(self) -> Union[None, HvacSystem]: """ Get HVAC system installed for this thermal zone From internal_zone :return: None or HvacSystem """ return self._parent_internal_zone.hvac_system @property def view_factors_matrix(self): """ Get thermal zone view factors matrix :return: [[float]] """ return self._view_factors_matrix @view_factors_matrix.setter def view_factors_matrix(self, value): """ Set thermal zone view factors matrix :param value: [[float]] """ self._view_factors_matrix = value @property def usage(self) -> Union[None, str]: """ Get thermal zone usage :return: None or str """ if self._parent_internal_zone.usage_zones is None: return None if self._usage is None: self._usage = '' for usage_zone in self._parent_internal_zone.usage_zones: self._usage += str(round(usage_zone.percentage) * 100) + '_' + usage_zone.usage + '_' self._usage = self._usage[:-1] return self._usage @usage.setter def usage(self, value): """ Set thermal zone usage :param value: str """ if value is not None: self._usage = str(value) @staticmethod def _get_schedule_of_day(requested_day_type, schedules): for schedule in schedules: for day_type in schedule.day_types: if day_type == requested_day_type: return schedule return None @property def not_detailed_source_mean_annual_internal_gains(self) -> Union[None, List[InternalGain]]: """ Get thermal zone internal gains with unknown energy source :return: [InternalGain] """ if self._parent_internal_zone.usage_zones is None: return None if self._not_detailed_source_mean_annual_internal_gains is None: _grouped_internal_gain = InternalGain() _grouped_internal_gain.type = 'grouped internal gains for thermal zone' _average_internal_gain = 0 _convective_part = 0 _radiative_part = 0 _latent_part = 0 for _usage_zone in self._parent_internal_zone.usage_zones: if _usage_zone.not_detailed_source_mean_annual_internal_gains is None: return None for _internal_gain in _usage_zone.not_detailed_source_mean_annual_internal_gains: _average_internal_gain += _internal_gain.average_internal_gain _convective_part += _internal_gain.average_internal_gain * _internal_gain.convective_fraction _radiative_part += _internal_gain.average_internal_gain * _internal_gain.radiative_fraction _latent_part += _internal_gain.average_internal_gain * _internal_gain.latent_fraction _grouped_internal_gain.average_internal_gain = _average_internal_gain if _average_internal_gain > 0: _grouped_internal_gain.convective_fraction = _convective_part / _average_internal_gain _grouped_internal_gain.radiative_fraction = _radiative_part / _average_internal_gain _grouped_internal_gain.latent_fraction = _latent_part / _average_internal_gain _internal_gains_reference = \ self._parent_internal_zone.usage_zones[0].not_detailed_source_mean_annual_internal_gains[0] if _internal_gains_reference.schedules is not None: day_types = [cte.MONDAY, cte.TUESDAY, cte.WEDNESDAY, cte.THURSDAY, cte.FRIDAY, cte.SATURDAY, cte.SUNDAY] schedule_of_days = [] for i_day in range(0, 7): schedule_of_day = \ copy.deepcopy(self._get_schedule_of_day(day_types[i_day], _internal_gains_reference.schedules)) if schedule_of_day is None: sys.stderr.write(f'Error. Not found requested day type when generating internal gains schedules ' f'in thermal_zone.\n') schedule_of_day.day_types = [day_types[i_day]] new_values = [] for i_value in range(0, len(_internal_gains_reference.schedules[0].values)): _new_value = 0 for _usage_zone in self._parent_internal_zone.usage_zones: for _internal_gain in _usage_zone.not_detailed_source_mean_annual_internal_gains: _value = self._get_schedule_of_day(day_types[i_day], _internal_gain.schedules) if _value is None: sys.stderr.write(f'Error. Not found requested day type when generating internal gains schedules ' f'in thermal_zone.\n') _value = _value.values[i_value] _new_value += _usage_zone.percentage * _value \ / len(_usage_zone.not_detailed_source_mean_annual_internal_gains) new_values.append(_new_value) schedule_of_day.values = new_values schedule_of_days.append(schedule_of_day) _grouped_internal_gain.schedules = schedule_of_days else: _grouped_internal_gain.convective_fraction = 0 _grouped_internal_gain.radiative_fraction = 0 _grouped_internal_gain.latent_fraction = 0 self._not_detailed_source_mean_annual_internal_gains = [_grouped_internal_gain] return self._not_detailed_source_mean_annual_internal_gains @not_detailed_source_mean_annual_internal_gains.setter def not_detailed_source_mean_annual_internal_gains(self, value): """ Set thermal zone internal gains with unknown energy source :param value: [InternalGain] """ self._not_detailed_source_mean_annual_internal_gains = value @property def hours_day(self) -> Union[None, float]: """ Get thermal zone usage hours per day :return: None or float """ if self._parent_internal_zone.usage_zones is None: return None if self._hours_day is None: self._hours_day = 0 for usage_zone in self._parent_internal_zone.usage_zones: self._hours_day += usage_zone.percentage * usage_zone.hours_day return self._hours_day @hours_day.setter def hours_day(self, value): """ Set thermal zone usage hours per day :param value: float """ if value is not None: self._hours_day = float(value) @property def days_year(self) -> Union[None, float]: """ Get thermal zone usage days per year :return: None or float """ if self._parent_internal_zone.usage_zones is None: return None if self._days_year is None: self._days_year = 0 for usage_zone in self._parent_internal_zone.usage_zones: self._days_year += usage_zone.percentage * usage_zone.days_year return self._days_year @days_year.setter def days_year(self, value): """ Set thermal zone usage days per year :param value: float """ if value is not None: self._days_year = float(value) @property def mechanical_air_change(self) -> Union[None, float]: """ Get thermal zone mechanical air change in air change per hour (ACH) :return: None or float """ if self._parent_internal_zone.usage_zones is None: return None if self._mechanical_air_change is None: self._mechanical_air_change = 0 for usage_zone in self._parent_internal_zone.usage_zones: if usage_zone.mechanical_air_change is None: return None self._mechanical_air_change += usage_zone.percentage * usage_zone.mechanical_air_change return self._mechanical_air_change @mechanical_air_change.setter def mechanical_air_change(self, value): """ Set thermal zone mechanical air change in air change per hour (ACH) :param value: float """ if value is not None: self._mechanical_air_change = float(value) @property def occupancy(self) -> Union[None, Occupancy]: """ Get occupancy in the thermal zone :return: None or Occupancy """ if self._parent_internal_zone.usage_zones is None: return None if self._occupancy is None: self._occupancy = Occupancy() _occupancy_density = 0 _convective_part = 0 _radiative_part = 0 _latent_part = 0 for usage_zone in self._parent_internal_zone.usage_zones: if usage_zone.occupancy is None: return None _occupancy_density += usage_zone.percentage * usage_zone.occupancy.occupancy_density if usage_zone.occupancy.sensible_convective_internal_gain is not None: _convective_part += usage_zone.percentage * usage_zone.occupancy.sensible_convective_internal_gain _radiative_part += usage_zone.percentage * usage_zone.occupancy.sensible_radiative_internal_gain _latent_part += usage_zone.percentage * usage_zone.occupancy.latent_internal_gain self._occupancy.occupancy_density = _occupancy_density self._occupancy.sensible_convective_internal_gain = _convective_part self._occupancy.sensible_radiative_internal_gain = _radiative_part self._occupancy.latent_internal_gain = _latent_part _occupancy_reference = self._parent_internal_zone.usage_zones[0].occupancy if _occupancy_reference.occupancy_schedules is not None: _schedules = [] for i_schedule in range(0, len(_occupancy_reference.occupancy_schedules)): schedule = copy.deepcopy(_occupancy_reference.occupancy_schedules[i_schedule]) new_values = [] for i_value in range(0, len(_occupancy_reference.occupancy_schedules[i_schedule].values)): _new_value = 0 for usage_zone in self._parent_internal_zone.usage_zones: _new_value += usage_zone.percentage * usage_zone.occupancy.occupancy_schedules[i_schedule].values[i_value] new_values.append(_new_value) schedule.values = new_values _schedules.append(schedule) self._occupancy.occupancy_schedules = _schedules return self._occupancy @occupancy.setter def occupancy(self, value): """ Set occupancy in the thermal zone :param value: Occupancy """ self._occupancy = value @property def lighting(self) -> Union[None, Lighting]: """ Get lighting information :return: None or Lighting """ if self._parent_internal_zone.usage_zones is None: return None if self._lighting is None: self._lighting = Lighting() _lighting_density = 0 _convective_part = 0 _radiative_part = 0 _latent_part = 0 for usage_zone in self._parent_internal_zone.usage_zones: if usage_zone.lighting is None: return None _lighting_density += usage_zone.percentage * usage_zone.lighting.density if usage_zone.lighting.convective_fraction is not None: _convective_part += usage_zone.percentage * usage_zone.lighting.density \ * usage_zone.lighting.convective_fraction _radiative_part += usage_zone.percentage * usage_zone.lighting.density \ * usage_zone.lighting.radiative_fraction _latent_part += usage_zone.percentage * usage_zone.lighting.density \ * usage_zone.lighting.latent_fraction self._lighting.density = _lighting_density if _lighting_density > 0: self._lighting.convective_fraction = _convective_part / _lighting_density self._lighting.radiative_fraction = _radiative_part / _lighting_density self._lighting.latent_fraction = _latent_part / _lighting_density else: self._lighting.convective_fraction = 0 self._lighting.radiative_fraction = 0 self._lighting.latent_fraction = 0 _lighting_reference = self._parent_internal_zone.usage_zones[0].lighting if _lighting_reference.schedules is not None: _schedules = [] for i_schedule in range(0, len(_lighting_reference.schedules)): schedule = copy.deepcopy(_lighting_reference.schedules[i_schedule]) new_values = [] for i_value in range(0, len(_lighting_reference.schedules[i_schedule].values)): _new_value = 0 for usage_zone in self._parent_internal_zone.usage_zones: _new_value += usage_zone.percentage * usage_zone.lighting.schedules[i_schedule].values[i_value] new_values.append(_new_value) schedule.values = new_values _schedules.append(schedule) self._lighting.schedules = _schedules return self._lighting @lighting.setter def lighting(self, value): """ Set lighting information :param value: Lighting """ self._lighting = value @property def appliances(self) -> Union[None, Appliances]: """ Get appliances information :return: None or Appliances """ if self._parent_internal_zone.usage_zones is None: return None if self._appliances is None: self._appliances = Appliances() _appliances_density = 0 _convective_part = 0 _radiative_part = 0 _latent_part = 0 for usage_zone in self._parent_internal_zone.usage_zones: if usage_zone.appliances is None: return None _appliances_density += usage_zone.percentage * usage_zone.appliances.density if usage_zone.appliances.convective_fraction is not None: _convective_part += usage_zone.percentage * usage_zone.appliances.density \ * usage_zone.appliances.convective_fraction _radiative_part += usage_zone.percentage * usage_zone.appliances.density \ * usage_zone.appliances.radiative_fraction _latent_part += usage_zone.percentage * usage_zone.appliances.density \ * usage_zone.appliances.latent_fraction self._appliances.density = _appliances_density if _appliances_density > 0: self._appliances.convective_fraction = _convective_part / _appliances_density self._appliances.radiative_fraction = _radiative_part / _appliances_density self._appliances.latent_fraction = _latent_part / _appliances_density else: self._appliances.convective_fraction = 0 self._appliances.radiative_fraction = 0 self._appliances.latent_fraction = 0 _appliances_reference = self._parent_internal_zone.usage_zones[0].appliances if _appliances_reference.schedules is not None: _schedules = [] for i_schedule in range(0, len(_appliances_reference.schedules)): schedule = copy.deepcopy(_appliances_reference.schedules[i_schedule]) new_values = [] for i_value in range(0, len(_appliances_reference.schedules[i_schedule].values)): _new_value = 0 for usage_zone in self._parent_internal_zone.usage_zones: _new_value += usage_zone.percentage * usage_zone.appliances.schedules[i_schedule].values[i_value] new_values.append(_new_value) schedule.values = new_values _schedules.append(schedule) self._appliances.schedules = _schedules return self._appliances @appliances.setter def appliances(self, value): """ Set appliances information :param value: Appliances """ self._appliances = value @staticmethod def _add_internal_gain(internal_gain_type, _internal_gain): _internal_gain.average_internal_gain = internal_gain_type.density _internal_gain.latent_fraction = internal_gain_type.latent_fraction _internal_gain.radiative_fraction = internal_gain_type.radiative_fraction _internal_gain.convective_fraction = internal_gain_type.convective_fraction _internal_gain.schedules = internal_gain_type.schedules def get_internal_gains(self) -> List[InternalGain]: """ Calculates and returns the list of all internal gains defined :return: [InternalGain] """ if self.not_detailed_source_mean_annual_internal_gains is not None: self._internal_gains = [] for _internal_gain in self.not_detailed_source_mean_annual_internal_gains: self._internal_gains.append(_internal_gain) if self.occupancy is not None: if self.occupancy.latent_internal_gain is not None: _internal_gain = InternalGain() _internal_gain.type = cte.OCCUPANCY _total_heat_gain = (self.occupancy.sensible_convective_internal_gain + self.occupancy.sensible_radiative_internal_gain + self.occupancy.latent_internal_gain) _internal_gain.average_internal_gain = _total_heat_gain if _total_heat_gain > 0: _internal_gain.latent_fraction = self.occupancy.latent_internal_gain / _total_heat_gain _internal_gain.radiative_fraction = self.occupancy.sensible_radiative_internal_gain / _total_heat_gain _internal_gain.convective_fraction = self.occupancy.sensible_convective_internal_gain / _total_heat_gain else: _internal_gain.latent_fraction = 0 _internal_gain.radiative_fraction = 0 _internal_gain.convective_fraction = 0 _internal_gain.schedules = self.occupancy.occupancy_schedules if self._internal_gains is not None: self._internal_gains.append(_internal_gain) else: self._internal_gains = [_internal_gain] if self.lighting is not None: _internal_gain = InternalGain() _internal_gain.type = cte.LIGHTING self._add_internal_gain(self.lighting, _internal_gain) if self._internal_gains is not None: self._internal_gains.append(_internal_gain) else: self._internal_gains = [_internal_gain] if self.appliances is not None: _internal_gain = InternalGain() _internal_gain.type = cte.APPLIANCES self._add_internal_gain(self.appliances, _internal_gain) if self._internal_gains is not None: self._internal_gains.append(_internal_gain) else: self._internal_gains = [_internal_gain] return self._internal_gains @property def thermal_control(self) -> Union[None, ThermalControl]: """ Get thermal control of this thermal zone :return: None or ThermalControl """ if self._parent_internal_zone.usage_zones is None: return None if self._thermal_control is None: self._thermal_control = ThermalControl() _mean_heating_set_point = 0 _heating_set_back = 0 _mean_cooling_set_point = 0 for usage_zone in self._parent_internal_zone.usage_zones: _mean_heating_set_point += usage_zone.percentage * usage_zone.thermal_control.mean_heating_set_point _heating_set_back += usage_zone.percentage * usage_zone.thermal_control.heating_set_back _mean_cooling_set_point += usage_zone.percentage * usage_zone.thermal_control.mean_cooling_set_point self._thermal_control.mean_heating_set_point = _mean_heating_set_point self._thermal_control.heating_set_back = _heating_set_back self._thermal_control.mean_cooling_set_point = _mean_cooling_set_point _thermal_control_reference = self._parent_internal_zone.usage_zones[0].thermal_control _types_reference = [] if _thermal_control_reference.hvac_availability_schedules is not None: _types_reference.append([cte.HVAC_AVAILABILITY, _thermal_control_reference.hvac_availability_schedules]) if _thermal_control_reference.heating_set_point_schedules is not None: _types_reference.append([cte.HEATING_SET_POINT, _thermal_control_reference.heating_set_point_schedules]) if _thermal_control_reference.cooling_set_point_schedules is not None: _types_reference.append([cte.COOLING_SET_POINT, _thermal_control_reference.cooling_set_point_schedules]) for i_type in range(0, len(_types_reference)): _schedules = [] _schedule_type = _types_reference[i_type][1] for i_schedule in range(0, len(_schedule_type)): schedule = copy.deepcopy(_schedule_type[i_schedule]) new_values = [] for i_value in range(0, len(_schedule_type[i_schedule].values)): _new_value = 0 for usage_zone in self._parent_internal_zone.usage_zones: if _types_reference[i_type][0] == cte.HVAC_AVAILABILITY: _new_value += usage_zone.percentage * \ usage_zone.thermal_control.hvac_availability_schedules[i_schedule].values[i_value] elif _types_reference[i_type][0] == cte.HEATING_SET_POINT: _new_value += usage_zone.percentage * \ usage_zone.thermal_control.heating_set_point_schedules[i_schedule].values[i_value] elif _types_reference[i_type][0] == cte.COOLING_SET_POINT: _new_value += usage_zone.percentage * \ usage_zone.thermal_control.cooling_set_point_schedules[i_schedule].values[i_value] new_values.append(_new_value) schedule.values = new_values _schedules.append(schedule) if i_type == 0: self._thermal_control.hvac_availability_schedules = _schedules elif i_type == 1: self._thermal_control.heating_set_point_schedules = _schedules elif i_type == 2: self._thermal_control.cooling_set_point_schedules = _schedules return self._thermal_control @thermal_control.setter def thermal_control(self, value): """ Set thermal control for this thermal zone :param value: ThermalControl """ self._thermal_control = value @property def total_floor_area(self): """ Get the total floor area of this thermal zone :return: float """ return self._total_floor_area @total_floor_area.setter def total_floor_area(self, value): """ Set the total floor area of this thermal zone :param value: float """ self._total_floor_area = value