Source code for eureca_building.ventilation

"""This module includes functions to model natural ventilation and infiltration
"""

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

import logging

import numpy as np
from scipy.optimize import fsolve

from eureca_building.schedule_properties import ventilation_prop
from eureca_building.schedule import Schedule
from eureca_building.fluids_properties import air_properties, gravitational_acceleration
from eureca_building.weather import WeatherFile
from eureca_building.surface import Surface

from eureca_building.exceptions import (
    InvalidScheduleType
)


[docs]def calc_neutral_plane_nat_vent(x, *data): """TODO : Giacomo per favore riempi questa Parameters ---------- x : float data : tuple """ a,b,c,h_t,h_b = data y = 0. for i in range(len(a)): y += (-2*a[i]*(np.abs(b*(x - h_t[i]) + c[i]))**(3/2)) / (3*b) y += (2*a[i]*(np.abs(b*(x - h_b[i]) + c[i]))**(3/2)) / (3*b) return y
[docs]class Ventilation: """Ventilation class """
[docs] def __init__( self, name: str, unit: str, nominal_value: float, schedule: Schedule, tag: str = None, ): """VentilationObject creation Parameters ---------- name : str name unit : str value of the unit: ["Vol/h", "kg/s", "kg/(m2 s)", "m3/s", "m3/(m2 s)"] nominal_value : float the value to be multiplied by the schedule schedule : eureca_building.schedule.Schedule Schedule object with a fractional schedule tag : str, default None a tag to define the type of internal load """ self.name = name self.unit = unit self.nominal_value = nominal_value self.schedule = schedule self.tag = tag
@property def nominal_value(self): return self._nominal_value @nominal_value.setter def nominal_value(self, value): try: value = float(value) except ValueError: raise ValueError(f"Ventilation object {self.name}, nominal value not float: {value}") self._nominal_value = value @property def schedule(self): return self._schedule @schedule.setter def schedule(self, value): if not isinstance(value, Schedule): raise ValueError(f"Ventilation object {self.name}, schedule type not Schedule: {type(value)}") if value.schedule_type not in ["dimensionless", ]: raise InvalidScheduleType( f"Ventilation object {self.name}, schedule type must be 'Dimensionless': {value.schedule_type}" ) if np.any(np.less(value.schedule, 0.)): raise ValueError( f"Ventilation object {self.name}, schedule type has some negative values" ) self._schedule = value @property def unit(self): return self._unit @unit.setter def unit(self, value): if not isinstance(value, str): raise ValueError(f"Ventilation object {self.name}, unit is not a string: {type(value)}") if value not in ventilation_prop["infiltration"]["unit"]: raise ValueError( f"Ventilation object {self.name}, unit must be chosen from: {ventilation_prop['infiltration']['unit']}" ) self._unit = value def _get_absolute_value_nominal(self, area=None, volume=None): """Returns ventilation nominal value in kg/s. This method might be overridden by a child class Parameters ---------- area : float, default None [m2]: must be provided if the load is area specific volume : float, default None [m3]: must be provided if the load is volume specific """ air_density = air_properties['density'] try: self.nominal_value_absolute = { "Vol/h": self.nominal_value * volume * air_density / 3600, "kg/s": self.nominal_value, "kg/(m2 s)": self.nominal_value * area, "m3/s": self.nominal_value * air_density, "m3/(m2 s)": self.nominal_value * area * air_density, }[self.unit] except TypeError: raise AttributeError( f"Ventilation object {self.name}, to calculate ventilation mass flow rate with specific unit you have to provide a volume or an area" ) except KeyError: raise ValueError( f"Ventilation object {self.name}, unit must be chosen from: {ventilation_prop['infiltration']['unit']}" )
[docs] def get_air_flow_rate(self, area=None, volume=None) -> np.array: """Returns ventilation air flow rate in kg/s. This method might be overridden by a child class Parameters ---------- area : float, default None [m2]: must be provided if the load is area specific volume : float, default None [m3]: must be provided if the load is volume specific Returns ---------- numpy.array air flow rate in kg/s """ # try: # self.nominal_value_absolute # except AttributeError: self._get_absolute_value_nominal(area=area, volume=volume) return self.nominal_value_absolute * self.schedule.schedule
[docs] def get_vapour_flow_rate(self, weather, area=None, volume=None) -> np.array: """Calc the vapour mass flow rate in kg/s Parameters ---------- weather : eureca_building.weather.WeatherFile Weather object area : float, default None [m2]: must be provided if the load is area specific volume : float, default None [m3]: must be provided if the load is volume specific Returns ---------- numpy.array vapour flow rate in kg/s """ # try: # self.nominal_value_absolute # except AttributeError: self._get_absolute_value_nominal(area=area, volume=volume) return self.nominal_value_absolute * self.schedule.schedule * weather.hourly_data['out_air_specific_humidity']
[docs] def get_flow_rate(self, weather, *args, **kwargs) -> list: """Return the air and vapour flow rate from natural ventilation. weather object must be passed Parameters ---------- weather : eureca_building.weather.WeatherFile Weather object args : list additional args kwargs : dict additional kwargs Returns ---------- tuple tuple of two numpy.array (air and vapour flow rates) """ if "area" not in kwargs.keys(): area = None else: area = kwargs['area'] if "volume" not in kwargs.keys(): volume = None else: volume = kwargs['volume'] vapuor = self.get_vapour_flow_rate(weather, area=area, volume=volume) air = self.get_air_flow_rate(area=area, volume=volume) return air, vapuor
[docs]class Infiltration(Ventilation): """This is just an inherited version of the Ventilation class, without any change """ pass
[docs]class NaturalVentilation(Ventilation): """Inheritaded from the Ventilation class """
[docs] def __init__( self, name: str, unit: str, nominal_value: float, schedule: Schedule, tag: str = None, surfaces_with_opening: list = None, weather: WeatherFile = None, ): """Init method. Call the Ventilation super() init and then store few more input Parameters ---------- name : str name unit : str value of the unit: ["Vol/h", "kg/s", "kg/(m2 s)", "m3/s", "m3/(m2 s)"] nominal_value : float the value to be multiplied by the schedule schedule : eureca_building.schedule.Schedule Schedule object with a fractional schedule tag : str, default None a tag to define the type of internal load surfaces_with_opening : list list of eureca_building.surface.Surface objects (those considered for the natural ventilation purposes weather : eureca_building.weather.WeatherFile Weather object """ super().__init__(name,unit,nominal_value,schedule,tag) self._get_windows_opening() if (weather != None) and (surfaces_with_opening != None): self.define_pressure_coef(weather, surfaces_with_opening)
@property def schedule(self): return self._schedule @schedule.setter def schedule(self, value): if not isinstance(value, Schedule): raise ValueError(f"Natural ventilation object {self.name}, schedule type not Schedule: {type(value)}") if value.schedule_type not in ["dimensionless",]: raise InvalidScheduleType( f"Natural ventilation object {self.name}, schedule type must be 'Dimensionless': {value.schedule_type}" ) if np.any(np.less(value.schedule, 0.)): raise ValueError( f"Natural ventilation object {self.name}, schedule type has some negative values" ) if np.any(np.greater(value.schedule, 1.)): raise ValueError( f"Natural ventilation object {self.name}, opening schedule type has some values above 1" ) self._schedule = value @property def unit(self): return self._unit @unit.setter def unit(self, value): if not isinstance(value, str): raise ValueError(f"Ventilation object {self.name}, unit is not a string: {type(value)}") if value not in ventilation_prop["natural"]["unit"]: raise ValueError( f"Ventilation object {self.name}, unit must be chosen from: {ventilation_prop['natural']['unit']}" ) self._unit = value def _get_absolute_value_nominal(self): """Returns natural ventilation nominal value in kg/s. Overrides the parent class method Parameters ---------- args kwargs """ air_density = air_properties['density'] try: self.nominal_value_absolute = { "%": self.nominal_value / 100, "-": self.nominal_value, }[self.unit] except TypeError: raise AttributeError( f"Natural Ventilation object {self.name}, to calculate natural ventilation the unit must be - or %" ) except KeyError: raise ValueError( f"Natural Ventilation object {self.name}, unit must be chosen from: {ventilation_prop['natural']['unit']}" )
[docs] def get_air_flow_rate(self): """Not implemented for Natural Ventilation """ raise NotImplementedError(f"Class Natural Ventilation: get_air_flow_rate method not implmented")
[docs] def get_vapour_flow_rate(self): """Not implemented for Natural Ventilation """ raise NotImplementedError(f"Class Natural Ventilation: get_vapour_flow_rate method not implmented")
[docs] def get_flow_rate(self): """Not implemented for Natural Ventilation """ raise NotImplementedError(f"Class Natural Ventilation: get_flow_rate method not implmented")
def _get_windows_opening(self) -> np.array: """Calc the windows opening from the input schedule in 0-1 range Returns ---------- numpy.array Wiondow opening schedule [0-1] """ try: self.nominal_value_absolute except AttributeError: self._get_absolute_value_nominal() self.windows_opening = self.nominal_value_absolute * self.schedule.schedule return self.windows_opening
[docs] def define_pressure_coef(self, weather, surfaces_with_opening): """TODO : Per Giacomo compila la documentazione Parameters ---------- weather : eureca_building.weather.WeatherFile WeatherFile object surfaces_with_opening : list list of eureca_building.surface.Surface objects (those considered for the natural ventilation purposes """ if not isinstance(weather, WeatherFile): raise ValueError(f"Natural Ventilation object: weather parameter is not of WeatherFile class: {type(weather)}") # TODO: check that number of layers is the same for each surface self.surfaces_with_opening = surfaces_with_opening windw_dir = weather.hourly_data["wind_direction"] surfaces_with_opening # Type list aspect_ratio = 1 pressure_coeff_fun = lambda alfa: (0.603*np.log(1.248 - 0.703*np.sin(alfa/2) -\ 1.175*(np.sin(alfa))**2 + \ 0.131*(np.sin(2*np.log(aspect_ratio)*alfa))**3 + \ 0.769*np.cos(alfa/2) +\ 0.07*np.log(aspect_ratio)**2*(np.sin(alfa/2)**2) + \ 0.717*np.cos(alfa/2)**2)) # def pressure_coeff_fun(alfa, aspect_ratio): # return (0.603*np.log(1.248 - 0.703*np.sin(alfa/2) -\ # 1.175*(np.sin(alfa))**2 + \ # 0.131*(np.sin(2*np.log(aspect_ratio)*alfa))**3 + \ # 0.769*np.cos(alfa/2) +\ # 0.07*np.log(aspect_ratio)**2*(np.sin(alfa/2)**2) + \ # 0.717*np.cos(alfa/2)**2)) for s in surfaces_with_opening: if not isinstance(s,Surface): raise TypeError(f"Natural Ventilation object: the list of surfaces with opening must be full of Surfaces objects. Surface type: {type(s)}") # Calcolo deì coefficient della supeficie azimuth_mod = s._azimuth + 180. angle_of_incidence = windw_dir - azimuth_mod - 180 angle_of_incidence[angle_of_incidence > 360] = angle_of_incidence[angle_of_incidence > 360] - 360 angle_of_incidence[angle_of_incidence > 360] = angle_of_incidence[angle_of_incidence > 360] - 360 angle_of_incidence[angle_of_incidence < 0] = angle_of_incidence[angle_of_incidence < 0] + 360 angle_of_incidence[angle_of_incidence < 0] = angle_of_incidence[angle_of_incidence < 0] + 360 angle_of_incidence[angle_of_incidence > 180] = 360 - angle_of_incidence[angle_of_incidence > 180] pressure_coeff = pressure_coeff_fun(angle_of_incidence/180*np.pi) s.wind_pressure_coeff = pressure_coeff s.angle_of_incidence = angle_of_incidence s._c_coeff = s.wind_pressure_coeff*weather.hourly_data["wind_speed"]**2 # Coeff c = wind pressure coeff * (wind speed)^2 for calculation of natural ventilation flow rate
[docs] def get_timestep_ventilation_mass_flow(self, ts, t_zone, weather): """TODO : Per Giacomo compila la documentazione Parameters ---------- ts : int time step of simulation t_zone : float zone temperature [°C] weather : eureca_building.weather.WeatherFile WeatherFile object """ # To be completed and commented opening_percentage = self.windows_opening[ts] a_coeff = [s._a_coeff * opening_percentage for s in self.surfaces_with_opening] b_coeff = (2*(t_zone - weather.hourly_data["out_air_db_temperature"][ts])*gravitational_acceleration)/(t_zone +273.15) # Coeff b = 2*(t_zona - t_esterna)*g/t_zona for calculation of natural ventilation flow rate c_coeff = [s._c_coeff[ts] for s in self.surfaces_with_opening] z_n = np.zeros(len(self.surfaces_with_opening[0]._h_bottom_windows)) vol_flow = np.zeros([len(self.surfaces_with_opening[0]._h_bottom_windows), len(self.surfaces_with_opening)]) vol_flow_sopra = np.zeros(len(self.surfaces_with_opening[0]._h_bottom_windows)) for floor in range(len(self.surfaces_with_opening[0]._h_bottom_windows)): h_top = [s._h_top_windows[floor] for s in self.surfaces_with_opening] h_bottom = [s._h_bottom_windows[floor] for s in self.surfaces_with_opening] #if abs(weather.hourly_data["out_air_db_temperature"][ts] - t_zone) >= 0.1: res = fsolve(calc_neutral_plane_nat_vent, 5., args = (a_coeff, b_coeff, c_coeff, h_top, h_bottom)) z_n[floor] = res[0] # else: # TODO: fix when T_ext and T_int are similar (only wind effect) # z_n[floor] = np.nan i = 0 for s in self.surfaces_with_opening: #if s._h_bottom_windows[floor] < z_n[floor]: vol_flow[floor][i] += (2*s._a_coeff * opening_percentage*(np.abs(b_coeff*(z_n[floor] - s._h_bottom_windows[floor]) + s._c_coeff[ts]))**(3/2)) / (3*b_coeff) #if s._h_top_windows[floor] < z_n[floor]: vol_flow[floor][i] -= (2*s._a_coeff * opening_percentage*(np.abs(b_coeff*(z_n[floor] - s._h_top_windows[floor]) + s._c_coeff[ts]))**(3/2)) / (3*b_coeff) i+=1 # vol_flow_sopra[floor] = 0 # for s in self.surfaces_with_opening: # if s._h_bottom_windows[floor] > z_n[floor]: # vol_flow_sopra[floor] -= (2*s._a_coeff * opening_percentage*(np.abs(b_coeff*(z_n[floor] - s._h_bottom_windows[floor]) + s._c_coeff[ts]))**(3/2)) / (3*b_coeff) # if s._h_top_windows[floor] > z_n[floor]: # vol_flow_sopra[floor] += (2*s._a_coeff * opening_percentage*(np.abs(b_coeff*(z_n[floor] - s._h_top_windows[floor]) + s._c_coeff[ts]))**(3/2)) / (3*b_coeff) return z_n, vol_flow, vol_flow_sopra
[docs]class MechanicalVentilation(Ventilation): """The same as Natural/Ventilation object but different check on units """ @property def unit(self): return self._unit @unit.setter def unit(self, value): if not isinstance(value, str): raise ValueError(f"Ventilation object {self.name}, unit is not a string: {type(value)}") if value not in ventilation_prop["mechanical"]["unit"]: raise ValueError( f"Ventilation object {self.name}, unit must be chosen from: {ventilation_prop['mechanical']['unit']}" ) self._unit = value def _get_absolute_value_nominal(self, area=None, volume=None): """Calcs ventilation nominal value in kg/s. This method overrides the parent class method Parameters ---------- area : float, default None [m2]: must be provided if the load is area specific volume : float, default None [m3]: must be provided if the load is volume specific """ air_density = air_properties['density'] try: self.nominal_value_absolute = { "Vol/h": self.nominal_value * volume * air_density / 3600, "kg/s": self.nominal_value, "kg/(m2 s)": self.nominal_value * area, "m3/s": self.nominal_value * air_density, "m3/(m2 s)": self.nominal_value * area * air_density, }[self.unit] except TypeError: raise AttributeError( f"Ventilation object {self.name}, to calculate ventilation mass flow rate with specific unit you have to provide a volume or an area" ) except KeyError: raise ValueError( f"Ventilation object {self.name}, unit must be chosen from: {ventilation_prop['mechanical']['unit']}" )