Source code for eureca_building.building

"""
This module includes functions to model the building and it includes the Building class
"""

__author__ = "Enrico Prataviera"
__credits__ = ["Enrico Prataviera"]
__license__ = "MIT"
__version__ = "0.1"
__maintainer__ = "Enrico Prataviera"

import copy
import logging
import os

import numpy as np
import pandas as pd

from eureca_building.config import CONFIG
from eureca_building.thermal_zone import ThermalZone
from eureca_building.weather import WeatherFile
from eureca_building.systems import hvac_heating_systems_classes, hvac_cooling_systems_classes, System
from eureca_building.exceptions import SimulationError
# %% Building class
[docs]class Building: """This class is a wrapper for ThermalZone objects and HVAC objects """
[docs] def __init__(self, name: str, thermal_zones_list:list, model:str = "2C"): """Constructor of the building class. Memorizes the attributes by means of properties setter. Checks also the validity of some attributes Parameters ---------- name : str Name of the building thermal_zone : list list of ThermalZone ibjects objects model : str, default 2C model to be used: 1C or 2C """ self.name = name self._thermal_zones_list = thermal_zones_list self._model = model
@property def _thermal_zones_list(self) -> list: return self.__thermal_zones_list @_thermal_zones_list.setter def _thermal_zones_list(self, value: list): try: value = list(value) except ValueError: raise TypeError(f"Building {self.name}, the thermal_zone_list must be a list or a tuple: {type(value)}") for tz in value: if not isinstance(tz, ThermalZone): raise TypeError(f"Building {self.name}, non ThermalZone object in thermal_zones_list. ") self.__thermal_zones_list = value @property def _model(self) -> str: return self.__model @_model.setter def _model(self, value: str): try: value = str(value) except ValueError: raise TypeError(f"Building {self.name}, the model must be a str: {type(value)}") if not value in ["1C","2C"]: raise TypeError(f"Building {self.name}, model must be 1C or 2C. Model = {value}") self.__model = value @property def heating_system(self) -> System: return self._heating_system @heating_system.setter def heating_system(self, value: System): if not isinstance(value, System): raise TypeError(f"Building {self.name}, the heating system must be a System object: {type(value)}") self._heating_system = value @property def cooling_system(self) -> System: return self._cooling_system @cooling_system.setter def cooling_system(self, value: System): if not isinstance(value, System): raise TypeError(f"Building {self.name}, the cooling system must be a System object: {type(value)}") self._cooling_system = value
[docs] def set_hvac_system(self, heating_system, cooling_system): f"""Sets using roperties the heating and cooling system type (strings) Available heating systems: {hvac_heating_systems_classes.keys()} Available cooling systems: {hvac_cooling_systems_classes.keys()} Parameters ---------- heating_system : str string to define building heating system cooling_system : str string to define building cooling system Raises ------ KeyError if the heating/cooling system is not included in the available list (See above) TypeError if the heating system does not comply with the Systems metaclass, which is necessary for simulations """ try: self.heating_system = hvac_heating_systems_classes[heating_system](heating_system_key = heating_system) except KeyError: raise KeyError(f"Building {self.name}, heating system not allowed: current heating system {heating_system}. Available heating systems:\n{hvac_heating_systems_classes.keys()}") if not isinstance(self.heating_system, System): raise TypeError((f"Building {self.name}, heating system does not comply with System class. The heating system class must be created using System interface")) try: self.cooling_system = hvac_cooling_systems_classes[cooling_system](cooling_system_key = cooling_system) except KeyError: raise KeyError(f"Building {self.name}, cooling system not allowed: current cooling system {cooling_system}. Available cooling systems:\n{hvac_cooling_systems_classes.keys()}") if not isinstance(self.cooling_system, System): raise TypeError((f"Building {self.name}, cooling system does not comply with System class. The cooling system class must be created using System interface")) # This overrides the convective and radiative fraction to zone for tz in self._thermal_zones_list: tz.heating_sigma = self.heating_system.sigma tz.cooling_sigma = self.cooling_system.sigma
[docs] def set_hvac_system_capacity(self, weather_object): f"""Calls the thermal zone heating and cooling capacity for all themrmal zones (must be run after the calculation of zone loads) Parameters ---------- weather_object : eureca_building.weather.WeatherFile WeatherFile object to use to simulate Raises ------ SimulationError if thermal_zone design load calculation has not been carried out yet """ heating_capacity, cooling_capacity = 0. ,0. try: for tz in self._thermal_zones_list: cooling_capacity += tz.design_sensible_cooling_system_power heating_capacity += tz.design_heating_system_power except AttributeError: raise SimulationError(f""" Building {self.name}: set_hvac_system_capacity method can run only after ThermalZones design load is calculated. Please run thermal zones design_sensible_cooling_load and design_heating_load """) self.heating_system.set_system_capacity(heating_capacity, weather_object) self.cooling_system.set_system_capacity(cooling_capacity, weather_object)
[docs] def solve_timestep(self, t: int, weather: WeatherFile): """Runs the thermal zone and hvac systems simulation for the timestep t Parameters ---------- t : int timestep weather_object : eureca_building.weather.WeatherFile WeatherFile object to use to simulate """ heat_load, cool_load, air_t, air_rh = 0., 0., 0., 0. for tz in self._thermal_zones_list: tz.solve_timestep(t, weather, model = self._model) air_t += tz.zone_air_temperature air_rh += tz.zone_air_rel_humidity if tz.sensible_zone_load > 0.: heat_load += tz.sensible_zone_load else: cool_load += tz.sensible_zone_load if tz.air_handling_unit.preh_deu_Dem > 0.: heat_load += tz.air_handling_unit.preh_deu_Dem else: cool_load += tz.air_handling_unit.preh_deu_Dem heat_load += tz.air_handling_unit.posth_Dem # For the moment not latent # if tz.latent_zone_load > 0.: # heat_load += tz.latent_zone_load # else: # cool_load += tz.latent_zone_load # if tz.latent_zone_load > 0.: # heat_load += tz.latent_zone_load # else: # cool_load += tz.latent_zone_load # DHW heat_load += tz.domestic_hot_water_demand[t] air_t /= len(self._thermal_zones_list) air_rh /= len(self._thermal_zones_list) self.heating_system.solve_system(heat_load, weather, t, air_t, air_rh) self.cooling_system.solve_system(cool_load, weather, t, air_t, air_rh)
[docs] def simulate(self, weather_object: WeatherFile, t_start: int = CONFIG.start_time_step, t_stop: int = CONFIG.final_time_step, preprocessing_ts: int = 100 * CONFIG.ts_per_hour, output_folder: str = None ): """Simulate a period and i stores the outputs. Calls solve_timestep method Parameters ---------- weather_object : eureca_building.weather.WeatherFile WeatherFile object to use to simulate (must be appliad after the calculation of zone loads t_start : int (Default first timestep of simulation) starting timestep t_stop : int (last timestep of simulation) stop timestep preprocessing_ts : int number of preprocessing timesteps output_folder : str, default None if not None prints building results in the selected folder Returns ---------- pandas.DataFrame building time step results """ for tz in self._thermal_zones_list: tz.reset_init_values() results = { 'TZ Ta [°C]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ To [°C]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ Tmr [°C]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ RH [-]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ sensible load [W]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ latent load [W]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ AHU pre heater load [W]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ AHU post heater load [W]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ DHW volume flow rate [L/s]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'TZ DHW demand [W]' : np.zeros([CONFIG.number_of_time_steps, len(self._thermal_zones_list)]), 'Heating system gas consumption [Nm3]' : np.zeros([CONFIG.number_of_time_steps, 1]), 'Heating system oil consumption [L]' : np.zeros([CONFIG.number_of_time_steps, 1]), 'Heating system wood consumption [kg]' : np.zeros([CONFIG.number_of_time_steps, 1]), 'Heating system electric consumption [Wh]' : np.zeros([CONFIG.number_of_time_steps, 1]), 'Cooling system electric consumption [Wh]': np.zeros([CONFIG.number_of_time_steps, 1]), 'Appliances electric consumption [Wh]': np.zeros([CONFIG.number_of_time_steps, 1]), } electric_consumption = np.array([tz.electric_load for tz in self._thermal_zones_list]).sum(axis=0) / CONFIG.ts_per_hour results['Appliances electric consumption [Wh]'][:, 0] = electric_consumption[CONFIG.start_time_step:CONFIG.final_time_step] results['TZ DHW volume flow rate [L/s]'] = 1000 * np.array([tz.domestic_hot_water_volume_flow_rate for tz in self._thermal_zones_list]).T[CONFIG.start_time_step:CONFIG.final_time_step] results['TZ DHW demand [W]'] = np.array([tz.domestic_hot_water_demand for tz in self._thermal_zones_list]).T[CONFIG.start_time_step:CONFIG.final_time_step] for t in range(t_start - preprocessing_ts, t_stop): self.solve_timestep(t, weather_object) results['TZ Ta [°C]'][t,:] = [tz.zone_air_temperature for tz in self._thermal_zones_list] results['TZ To [°C]'][t,:] = [tz.zone_operative_temperature for tz in self._thermal_zones_list] results['TZ Tmr [°C]'][t,:] = [tz.zone_mean_radiant_temperature for tz in self._thermal_zones_list] results['TZ RH [-]'][t,:] = [tz.zone_air_rel_humidity for tz in self._thermal_zones_list] results['TZ sensible load [W]'][t, :] = [tz.sensible_zone_load for tz in self._thermal_zones_list] results['TZ latent load [W]'][t, :] = [tz.latent_zone_load for tz in self._thermal_zones_list] results['TZ AHU pre heater load [W]'][t, :] = [tz.air_handling_unit.preh_deu_Dem for tz in self._thermal_zones_list] results['TZ AHU post heater load [W]'][t, :] = [tz.air_handling_unit.posth_Dem for tz in self._thermal_zones_list] results['Heating system gas consumption [Nm3]'][t,0] = self.heating_system.gas_consumption results['Heating system oil consumption [L]'][t,0] = self.heating_system.oil_consumption results['Heating system wood consumption [kg]'][t,0] = self.heating_system.wood_consumption results['Heating system electric consumption [Wh]'][t,0] = self.heating_system.electric_consumption results['Cooling system electric consumption [Wh]'][t,0] = self.cooling_system.electric_consumption # Saving results tz_labels = [res for res in results.keys() if res.startswith("TZ")] bd_labels = [res for res in results.keys() if not res.startswith("TZ")] tz_names = [tz.name for tz in self._thermal_zones_list] columns_tz = pd.MultiIndex.from_product([tz_labels,tz_names]) columns_bd = pd.MultiIndex.from_product([bd_labels,[f"Bd {self.name}"]]) tz = pd.DataFrame(0., index = range(CONFIG.number_of_time_steps), columns = columns_tz) bd = pd.DataFrame(0., index = range(CONFIG.number_of_time_steps), columns = columns_bd) total = pd.concat([bd, tz], axis=1) for tz_result_label in tz_labels: total[tz_result_label] = results[tz_result_label] for bd_result_label in bd_labels: total[bd_result_label] = results[bd_result_label] if output_folder != None: if not os.path.isdir(output_folder): os.mkdir(output_folder) total.to_csv(os.path.join(output_folder, f"Results {self.name}.csv"), float_format='%.2f', index = False) return total
[docs] def get_geojson_feature_parser(self): """Function to get the json dictionary of building properties to stamp the output geojson Returns ---------- dict dict with some info of the building """ floors = [] for s in self._thermal_zones_list[0]._surface_list: if s.surface_type == "GroundFloor": vtxs = s._vertices vtxs = [[vtx[0], vtx[1]] for vtx in vtxs] floors.append(vtxs) return { "type": "Feature", "properties": { "id": self.name, "new_id": self.name, "Name": self.name, }, "geometry":{ "type": "MultiPolygon", "coordinates": [floors] } }