"""
This module includes functions and classes to model internal heat gains
"""
__author__ = "Enrico Prataviera"
__credits__ = ["Enrico Prataviera"]
__license__ = "MIT"
__version__ = "0.1"
__maintainer__ = "Enrico Prataviera"
import logging
import numpy as np
from eureca_building.schedule_properties import internal_loads_prop
from eureca_building.schedule import Schedule
from eureca_building.fluids_properties import vapour_properties
from eureca_building.exceptions import (
ConvectiveRadiantFractionError,
InvalidHeatGainUnit,
InvalidScheduleType,
AreaNotProvided,
PeopleNotProvided,
)
# TODO: implement interface for methods to be implemented
# https://realpython.com/python-interface/
[docs]class InternalLoad:
"""Internal Gain Class: parent class to set some common things
"""
[docs] def __init__(
self,
name: str,
nominal_value: float,
schedule: Schedule,
tag: str = None,
):
"""Parent class for some inherited InternalLoads.
It load the input and checks them throughout properties setter
Parameters
----------
name : str
name
nominal_value : float
the value to be multiplied by the schedule
schedule : eureca_building.schedule.Schedule
Schedule object
tag : str
a tag to define the type of internal load
"""
self.name = name
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"Internal Heat Gain {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"Internal Heat Gain {self.name}, schedule type not Schedule: {type(value)}")
if value.schedule_type not in ["dimensionless", "percent", ]:
raise InvalidScheduleType(
f"Internal Heat Gain {self.name}, schedule type must be 'Dimensionless' or 'Percent': {value.schedule_type}"
)
self._schedule = value
@property
def fraction_to_zone(self):
return self._fraction_to_zone
@fraction_to_zone.setter
def fraction_to_zone(self, value):
if value < 0 or value > 1.:
raise ValueError(f"Internal Heat Gain {self.name}, fraction to zone outside range [0.,1.]: {value}")
self._fraction_to_zone = float(value)
@property
def fraction_latent(self):
return self._fraction_latent
@fraction_latent.setter
def fraction_latent(self, value):
if value < 0 or value > 1.:
raise ValueError(f"Internal Heat Gain {self.name}, fraction latent outside range [0.,1.]: {value}")
self._fraction_latent = float(value)
@property
def fraction_radiant(self):
return self._fraction_radiant
@fraction_radiant.setter
def fraction_radiant(self, value):
if value < 0 or value > 1.:
raise ValueError(f"Internal Heat Gain {self.name}, fraction radiant outside range [0.,1.]: {value}")
try:
if abs(value + self._fraction_convective - 1.) > 1e-5:
raise ConvectiveRadiantFractionError(
f"Internal Heat Gain {self.name}, radiant/convective fraction sum not 1. Radiant = {value}, convective = {self.fraction_convective}"
)
except AttributeError:
# This is just to avoid the check if self.fraction_radiant doesn't exist
pass
self._fraction_radiant = float(value)
@property
def fraction_convective(self):
return self._fraction_convective
@fraction_convective.setter
def fraction_convective(self, value):
if value < 0 or value > 1.:
raise ValueError(f"Internal Heat Gain {self.name}, fraction convective outside range [0.,1.]: {value}")
try:
if abs(value + self.fraction_radiant - 1.) > 1e-5:
raise ConvectiveRadiantFractionError(
f"Internal Heat Gain {self.name}, radiant/convective fraction sum not 1. Convective = {value}, radiant = {self.fraction_radiant}"
)
except AttributeError:
# This is just to avoid the check if self.fraction_radiant doesn't exist
pass
self._fraction_convective = float(value)
[docs] def get_convective_load(self, *args, **kwarg) -> np.array:
"""Just an empty method to raise an NotImplementedError Exception. This way any inherited class implements it
Parameters
----------
args
kwarg
"""
raise NotImplementedError(
f"""
You must override the get_convective_load method for each class inherited from InternalLoad
Return value must be a np.array
"""
)
[docs] def get_radiant_load(self, *args, **kwarg) -> np.array:
"""Just an empty method to raise an NotImplementedError Exception. This way any inherited class implements it
Parameters
----------
args
kwarg
"""
raise NotImplementedError(
f"""
You must override the get_radiant_load method for each class inherited from InternalLoad
Return value must be a np.array
"""
)
[docs] def get_latent_load(self, *args, **kwarg) -> np.array:
"""Just an empty method to raise an NotImplementedError Exception. This way any inherited class implements it
Parameters
----------
args
kwarg
"""
raise NotImplementedError(
f"""
You must override the get_latent_load method for each class inherited from InternalLoad
Return value must be a np.array
"""
)
[docs] def get_loads(self, *args, **kwargs) -> list:
"""Return the convective, radiant, latent, electric load (numpy.array)
If the calculation method is specific (W/m2 or px/m2) the area must be passed as kwarg (example area=12.5)
Parameters
----------
area : float
Area in m2. pass it as kwarg: load_obj.get_loads(area = 12.5)
Parameters
----------
tuple
[numpy.array, numpy.array, numpy.array, numpy.array]
the schedules: convective [W], radiant [W], vapour [kg_vap/s], electric [W]
"""
if "area" not in kwargs.keys():
area = None
else:
area = kwargs['area']
conv = self.get_convective_load(area=area)
rad = self.get_radiant_load(area=area)
lat = self.get_latent_load(area=area)
el = self.get_electric_load(area=area)
return conv, rad, lat, el
[docs]class People(InternalLoad):
[docs] def __init__(
self,
name: str,
nominal_value: float,
unit: str,
schedule: Schedule,
fraction_latent: float = 0.55,
fraction_radiant: float = 0.3,
fraction_convective: float = 0.7,
metabolic_rate: float = 110,
tag: str = None,
):
f"""Inherited from InternalLoad class. CHecks the values throughout propeties setter methods
The sum of radiant and convective fraction must be 1
Parameters
----------
name : str
name
nominal_value : float
the value to be multiplied by the schedule
unit : str
define the unit from the list {internal_loads_prop["people"]["unit"]}
schedule : eureca_building.schedule.Schedule
Schedule object
fraction_latent : float, default 0.55
latent fraction (between 0 and 1)
fraction_radiant : float, default 0.3
radiant fraction of the sensible part (between 0 and 1)
fraction_convective : float, default 0.7
convective fraction of the sensible part (between 0 and 1)
metabolic_rate : float, default 110
Metabolic rate [W/px]
tag : str, default None
Tag string to define the type of load
"""
super().__init__(name, nominal_value, schedule, tag=tag)
self.unit = unit
self.fraction_latent = fraction_latent
self.fraction_radiant = fraction_radiant
self.fraction_convective = fraction_convective
self.metabolic_rate = metabolic_rate
@property
def unit(self):
return self._unit
@unit.setter
def unit(self, value):
if not isinstance(value, str):
raise TypeError(f"People load {self.name}, type is not a str: {value}")
if value not in internal_loads_prop["people"]["unit"]:
raise InvalidHeatGainUnit(
f"People load {self.name}, unit not in: {internal_loads_prop['people']['unit']}\n{value}"
)
if value in ["W/m2", "px/m2", ]:
self._calculation_method = "floor_area"
elif value in ["W", "px", ]:
self._calculation_method = "absolute"
self._unit = value
@property
def metabolic_rate(self):
return self._metabolic_rate
@metabolic_rate.setter
def metabolic_rate(self, value):
try:
value = float(value)
except ValueError:
raise ValueError(f"People load {self.name}, metabolic_rate is not a float: {value}")
if value < 0.:
raise ValueError(
f"People load {self.name}, negative metabolic rate: {value}"
)
if value > 250.:
logging.warning(
f"People load {self.name}, metabolic rate over 250 W/px: {value}"
)
self._metabolic_rate = value
def _get_absolute_value_nominal(self, area=None):
"""Memorizes the occupancy nominal value in W
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provided if the load is specific
"""
if self._calculation_method == "floor_area":
if area == None:
raise AreaNotProvided(
f"Internal Heat Gain {self.name}, specific load but area not provided."
)
area = float(area)
else:
area = 1.
if self.unit in ["px", "px/m2", ]:
px_w_converter = self.metabolic_rate
else:
px_w_converter = 1.
# This value is in W
self.nominal_value_absolute = area * px_w_converter * self.nominal_value
[docs] def get_convective_load(self, area=None):
"""Returns the convective load numpy.array
If the calculation method is specific (W/m2 or px/m2) the area must be passed
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provided if the load is specific
Returns
----------
numpy.array
the schedule [W]
"""
# try:
# self.nominal_value_absolute
# except AttributeError:
self._get_absolute_value_nominal(area=area)
convective_fraction = (1 - self.fraction_latent) * self.fraction_convective
return convective_fraction * self.nominal_value_absolute * self.schedule.schedule
[docs] def get_radiant_load(self, area=None):
"""Returns the radiant load numpy.array
If the calculation method is specific (W/m2 or px/m2) the area must be passed
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provide if the load is specific
Returns
----------
numpy.array
the schedule [W]
"""
# try:
# self.nominal_value_absolute
# except AttributeError:
self._get_absolute_value_nominal(area=area)
radiant_fraction = (1 - self.fraction_latent) * self.fraction_radiant
return radiant_fraction * self.nominal_value_absolute * self.schedule.schedule
[docs] def get_latent_load(self, area=None):
"""Return the latent load numpy.array
If the calculation method is specific (W/m2 or px/m2) the area must be passed
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provided if the load is specific
Returns
----------
numpy.array
the schedule [W]
"""
# try:
# self.nominal_value_absolute
# except AttributeError:
self._get_absolute_value_nominal(area=area)
vapour_nominal_value_kg_s = self.fraction_latent * self.nominal_value_absolute / vapour_properties[
'latent_heat']
return self.schedule.schedule * vapour_nominal_value_kg_s
[docs] def get_electric_load(self, area=None):
"""Return the electric consumption load numpy.array
If the calculation method is specific (W/m2 or px/m2) the area must be passed
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provided if the load is specific
Returns
----------
numpy.array
the schedule [W]
"""
# try:
# self.nominal_value_absolute
# except AttributeError:
self._get_absolute_value_nominal(area=area)
return self.nominal_value_absolute * self.schedule.schedule * 0
[docs]class ElectricLoad(InternalLoad):
[docs] def __init__(
self,
name: str,
nominal_value: float,
unit: str,
schedule: Schedule,
fraction_to_zone: float = 1.,
fraction_radiant: float = 0.3,
fraction_convective: float = 0.7,
number_of_people: float = None,
tag: str = None,
):
f"""Inherited from InternalLoad class. Uses properties set methods to check types
The sum of radiant and convective fraction must be 1
Parameters
----------
name : str
name
nominal_value : float
the value to be multiplied by the schedule
unit : str
define the unit from the list {internal_loads_prop["people"]["unit"]}
schedule : eureca_building.schedule.Schedule
Schedule object
fraction_to_zone : float, default 1.
fraction that is actually counted as Heat Load for the zone (between 0 and 1)
fraction_radiant : float, default 0.3
radiant fraction of the sensible part (between 0 and 1)
fraction_convective : float, default 0.7
convective fraction of the sensible part (between 0 and 1)
number_of_people : float, default None
if the unit is W/px then this number must be passed
tag : str, default None
Tag string to define the type of load
"""
super().__init__(name, nominal_value, schedule, tag=tag)
self.unit = unit
self.fraction_to_zone = fraction_to_zone
self.fraction_radiant = fraction_radiant
self.fraction_convective = fraction_convective
self.number_of_people = number_of_people
@property
def unit(self):
return self._unit
@unit.setter
def unit(self, value):
if not isinstance(value, str):
raise TypeError(f"ElectricLoad load {self.name}, type is not a str: {value}")
if value not in internal_loads_prop["electric"]["unit"]:
raise InvalidHeatGainUnit(
f"ElectricLoad load {self.name}, unit not in: {internal_loads_prop['people']['unit']}\n{value}"
)
if value in ["W/m2"]:
self._calculation_method = "floor_area"
elif value in ["W/px"]:
self._calculation_method = "people"
elif value in ["W"]:
self._calculation_method = "absolute"
self._unit = value
@property
def number_of_people(self):
return self._number_of_people
@number_of_people.setter
def number_of_people(self, value):
if value is not None:
try:
value = value
except ValueError:
raise ValueError(f"ElectricLoad load {self.name}, number_of_people is not a float: {value}")
if value < 0.:
raise ValueError(
f"ElectricLoad load {self.name}, negative number_of_people: {value}"
)
self._number_of_people = value
def _get_absolute_value_nominal(self, area=None):
"""Memorizes the electric load nominal value in W
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provided if the load is specific
"""
if self._calculation_method == "floor_area":
if area == None:
raise AreaNotProvided(
f"Internal Heat Gain {self.name}, specific load but area not provided."
)
area = float(area)
else:
area = 1.
if self._calculation_method == "people":
if self.number_of_people == None:
raise PeopleNotProvided(
f"Internal Heat Gain {self.name}, people calculation but number of people not provided."
)
number_of_people = float(self.number_of_people)
else:
number_of_people = 1.
# print(area)
# print(number_of_people)
# This value is in W
self.nominal_value_absolute = number_of_people * area * self.nominal_value
[docs] def get_convective_load(self, area=None):
"""Returns the convective load numpy.array
If the calculation method is specific (W/m2 or px/m2) the area must be passed
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provided if the load is specific
Returns
----------
numpy.array
the schedule [W]
"""
# try:
# self.nominal_value_absolute
# except AttributeError:
self._get_absolute_value_nominal(area=area)
convective_fraction = self.fraction_to_zone * self.fraction_convective
return convective_fraction * self.nominal_value_absolute * self.schedule.schedule
[docs] def get_radiant_load(self, area=None):
"""Returns the radiant load numpy.array
If the calculation method is specific (W/m2 or px/m2) the area must be passed
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provide if the load is specific
Returns
----------
numpy.array
the schedule [W]
"""
# try:
# self.nominal_value_absolute
# except AttributeError:
self._get_absolute_value_nominal(area=area)
radiant_fraction = self.fraction_to_zone * self.fraction_radiant
return radiant_fraction * self.nominal_value_absolute * self.schedule.schedule
[docs] def get_latent_load(self, area=None):
"""Return the latent load numpy.array
If the calculation method is specific (W/m2 or px/m2) the area must be passed
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provided if the load is specific
Returns
----------
numpy.array
the schedule [W]
"""
return self.schedule.schedule * 0
[docs] def get_electric_load(self, area=None):
"""Return the electric consumption load numpy.array
If the calculation method is specific (W/m2 or px/m2) the area must be passed
Parameters
----------
area : float, default None
Area of the zone in [m2]: must be provided if the load is specific
Returns
----------
numpy.array
the schedule [W]
"""
# try:
# self.nominal_value_absolute
# except AttributeError:
self._get_absolute_value_nominal(area=area)
return self.nominal_value_absolute * self.schedule.schedule
[docs]class Lights(ElectricLoad):
[docs] def __init__(
self,
name: str,
nominal_value: float,
unit: str,
schedule: Schedule,
fraction_to_zone: float = 1.,
fraction_radiant: float = 0.3,
fraction_convective: float = 0.7,
number_of_people: float = None,
tag: str = None,
):
f"""Inherited from ElectricLoad class. Uses properties set methods to check types
This is just a wrapper for an EletricLoad object as they are similar
Parameters
----------
name : str
name
nominal_value : float
the value to be multiplied by the schedule
unit : str
define the unit from the list {internal_loads_prop["people"]["unit"]}
schedule : eureca_building.schedule.Schedule
Schedule object
fraction_to_zone : float, default 1.
fraction that is actually counted as Heat Load for the zone (between 0 and 1)
fraction_radiant : float, default 0.3
radiant fraction of the sensible part (between 0 and 1)
fraction_convective : float, default 0.7
convective fraction of the sensible part (between 0 and 1)
number_of_people : float, default None
if the unit is W/px then this number must be passed
tag : str, default None
Tag string to define the type of load
"""
super().__init__(
name,
nominal_value,
unit,
schedule,
fraction_to_zone=fraction_to_zone,
fraction_radiant=fraction_radiant,
fraction_convective=fraction_convective,
number_of_people=number_of_people,
tag=tag,
)