"""This module contains UseConditions class."""
import random
from builtins import ValueError
import pandas as pd
from itertools import cycle, islice
from collections import OrderedDict
import teaser.data.input.usecond_input as usecond_input
import teaser.data.output.usecond_output as usecond_output
from teaser.logic.utilities import division_from_json
import warnings
[docs]
class UseConditions(object):
"""UseConditions class contains all zone specific boundary conditions.
Class that contains the boundary conditions of use for buildings defined in
DIN V 18599-10 ( :cite:`DeutschesInstitutfurNormung.2016`) and VDI 2078
(:cite:`VereinDeutscherIngenieure.2015c`). Profiles for internal gains (
persons, lighting, machines) are taken from SIA2024 (
:cite:`SwissSocietyofEngineersandArchitects.March2006`). In addition some
TEASER specific use conditions have been attached to this class. The
docstring also contains how the use conditions is used.
Note: Most attributes description are translations from DIN V 18599-10
standard
Attributes
----------
usage: str
usage type
AixLib usage: String to distinguish usages of a zone
typical_length: float [m]
typical length of a room in a usage zone. This value is taken from
SIA 2024. Archetype usage: division of usage zones in rooms
typical width: float [m]
typical width of a usage zone. This value is taken from
SIA 2024. Archetype usage: division of usage zones in rooms
with_heating: boolean
Sets if the zone is heated by ideal heater or not.
with_cooling: boolean
Sets if the zone is cooled by ideal cooler or not.
with_ideal_thresholds: boolean
Sets if the threshold temperatures for ideal heater and cooler are
used.
True = T_threshold_heating and T_threshold_cooling are used.
This can, in most cases, prevent simultaneous heating from AHU and
cooling from ideal heater and vice versa. This should only be turned
on if an AHU exists.
T_threshold_heating: float [K]
Threshold for the outside temperature above which the ideal heater is
permanently shut down regardless the inside temperature.
Default is 15 °C which corresponds to the value for all buildings
that are not built
according to EnEV standard according to DIN EN 18599-5.
T_threshold_cooling: float [K]
Threshold for the outside temperature below which the ideal cooler is
permanently shut down regardless the inside temperature.
Default is 22 °C, since there are no european standards
for cooling degree days this value is taken from the following paper:
"Heating Degree Days, Cooling Degree Days and Precipitation in Europe
—analysis for the CELECT-project" by Benestad, 2008.
heating_profile : list [K]
Heating setpoint, regarding the zone temperature, of ideal static
heating for a day or similar. You can set a list of any
length, TEASER will multiplicate this list for one whole year.
cooling_profile : list [K]
Cooling setpoint, regarding the zone temperature, of ideal static
cooling for a day or similar. You can set a list of any
length, TEASER will multiplicate this list for one whole year.
fixed_heat_flow_rate_persons: float [W/person]
fixed heat flow rate for one person in case of temperature
independent calculation. Default value is 70
W/person and describes
the maximum heat flow rate depending on the schedule.
persons : float [Persons/m2]
Specific number of persons per square area.
Annex: Used for internal gains
internal_gains_moisture_no_people : float [g/(h m²)]
internal moisture production of plants, etc. except from people.
activity_degree_persons : float [met]
default value is 1.2 met
AixLib: used for heat flow rate calculation (internal_gains_mode=1)
or heat flow rate, moisture and co2 gains (internal_gains_mode=3). Both
are temperature and activity degree depending, calculation based
on SIA2024 (2015) and Engineering ToolBox (2004).
Annex: not used, heat flow rate is constant value
fixed_heat_flow_rate_persons
ratio_conv_rad_persons: float
describes the ratio between convective and radiative heat transfer
of the persons [convective/radiative]. Default values are derived from
:cite:`VereinDeutscherIngenieure.2015c`.
AixLib: Used in Zone record for internal gains
Annex: Used for internal gains
persons_profile: list
Relative presence of persons 0-1 (e.g. 0.5 means that 50% of the total
number of persons are currently in the room). Given
for 24h. This value is taken from SIA 2024. You can set a list of any
length, TEASER will multiplicate this list for one whole year.
AixLib: Used for internal gains profile on top-level
Annex: Used for internal gains
machines: float [W/m2]
area specific eletrical load of machines per m2. This value is taken
from SIA 2024 and DIN V 18599-10 for medium occupancy.
AixLib: Used in Zone record for internal gains,
internalGainsMachinesSpecific
Annex: Used for internal gains
ratio_conv_rad_machines: float
describes the ratio between convective and radiative heat transfer
of the machines [convective/radiative]. Default values are derived from
:cite:`Davies.2004`.
AixLib: Used in Zone record for internal gains
Annex: Not used, all machines are convective (see Annex examples)
machines_profile: list
Relative presence of machines 0-1 (e.g. 0.5 means that 50% of the total
number of machines are currently used in the room). Given
for 24h. This value is taken from SIA 2024. You can set a list of any
length, TEASER will multiplicate this list for one whole year.
AixLib: Used for internal gains profile on top-level
Annex: Used for internal gains
use_maintained_illuminance: bool
decision variable to determine wether lighting_power will be given by
fixed_lighting_power or by calculation using the variables maintained_illuminance
and lighting_efficiency_lumen
lighting_power: float [W/m2]
spec. electr. Power for lighting
Determined by use_maintained_illuminance
Not needed in input json file
AixLib: Used in Zone record for internal gains
Annex: Not used (see Annex examples)
fixed_lighting_power: float [W/m2]
spec. fixed electrical power for lighting. This value is taken from SIA 2024.
ratio_conv_rad_lighting : float
describes the ratio between convective and radiative heat transfer
of the lighting [convective/radiative]. Default values are derived from
:cite:`DiLaura.2011`.
AixLib: Used in Zone record for internal gains, lighting
maintained_illuminance : float [Lx]
maintained illuminance value for lighting.
This value is partially taken from SIA 2024 (2015-10) and partially
from DIN V EN 18599-10 (2018-09).
lighting_efficiency_lumen: float [lm/W_el]
lighting efficiency in lm/W_el, in german: Lichtausbeute
lighting_profil : [float]
Relative presence of lighting 0-1 (e.g. 0.5 means that 50% of the total
lighting power are currently used). Typically given for 24h. This is
aligned to the user profile.
AixLib: Used for internal gains profile on top-level
Annex: Not used (see Annex examples)
min_ahu: float [m3/(m2*h)]
Zone specific minimum specific air flow supplied by the AHU.
AixLib: Used on Multizone level for central AHU to determine total
volume flow of each zone.
- **Note**: The AixLib parameter "WithProfile" determines whether the
(v_flow_profile combined with "min_ahu and max_ahu") or the
(persons_profile combined with "min_ahu and max_ahu")
is used for the AHU supply flow calculations.
Per default: (v_flow_profile combined with "min_ahu and max_ahu")
max_ahu : float [m3/(m2*h)]
Zone specific maximum specific air flow supplied by the AHU.
AixLib: Used on Multizone level for central AHU to determine total
volume flow of each zone.
- **Note**: The AixLib parameter "WithProfile" determines whether the
(v_flow_profile combined with "min_ahu and max_ahu") or the
(persons_profile combined with "min_ahu and max_ahu")
is used for the AHU supply flow calculations.
Per default: (v_flow_profile combined with "min_ahu and max_ahu")
with_ahu : boolean
Zone is connected to central air handling unit or not
AixLib: Used on Multizone level for central AHU.
use_constant_infiltration : boolean
choose whether window opening should be regarded.
true = natural infiltration + ventilation due to a AHU
false = natural infiltration + ventilation due to a AHU
+ window infiltration calculated by window opening model
AixLib: Used on Zone level for ventilation.
normative_infiltration: float [1/h]
Infiltration rate for static heat load calculation.
Default is 0.5 based on the DIN EN 12831-1:2017 minimal air exchange rate reference value.
base_infiltration : float [1/h]
base value for the natural infiltration without window openings
AixLib: Used on Zone level for ventilation.
max_user_infiltration : float [1/h]
Additional infiltration rate for maximum persons activity
AixLib: Used on Zone level for ventilation.
max_overheating_infiltration : list [1/h]
Additional infiltration rate when overheating appears
AixLib: Used on Zone level for ventilation.
max_summer_infiltration : list
Additional infiltration rate in the summer with
[infiltration_rate [1/h], Tmin [K], Tmax [K]]. Default values are
aligned to :cite:`DINV1859910`.
AixLib: Used on Zone level for ventilation.
winter_reduction_infiltration : list
Reduction factor of userACH for cold weather with
[infiltration_rate [1/h], Tmin [K], Tmax [K]]
AixLib: Used on Zone level for ventilation.
Default values are
aligned to :cite:`DINV1859910`.
schedules: pandas.DataFrame
All time dependent boundary attributes in one pandas DataFrame, used
for export (one year in hourly timestamps.) Derived from json.
Schedules can be adjusted by setting the following parameters:
- adjusted_opening_times
- first_saturday_of_year
- profiles_weekend_factor
- set_back_times
- heating_set_back
- cooling_set_back
To take adjustments into account you need to call calc_schedules()
function afterwards.
Note: python attribute, not customizable by user (derived from Json)
adjusted_opening_times: list
Sets the first and last hour of opening. These will cut or extend the
existing profiles (machines, lights, persons).
[opening_hour, closing_hour]
first_saturday_of_year: int
Weekday number of first saturday of the year [1:monday;7:tuesday].
Is needed to calc which days of profile should be reduced by
profiles_weekend_factor.
profiles_weekend_factor: float
Factor to scale the existing profiles on weekends. For a reduction use
values between [0;1]. Increase is also possible.
set_back_times: list
Sets the first and last hour outside of which the offset is applied.
List of two integers [first_hour, last_hour]
heating_set_back: float [K]
Set back temperature offset for heating profile. Positive (+) values
increase the profile, negative (-) decrease.
cooling_set_back: float [K]
Set back temperature offset for cooling profile. Positive (+) values
increase the profile, negative (-) decrease.
"""
def __init__(self, parent=None):
"""Construct UseConditions."""
self.internal_id = random.random()
self.parent = parent
self.usage = "Single office"
self.typical_length = 6.0
self.typical_width = 6.0
self.with_heating = True
self.with_cooling = False
self.T_threshold_heating = 288.15
self.T_threshold_cooling = 295.15
self.fixed_heat_flow_rate_persons = 70
self.activity_degree_persons = 1.2
self._persons = 1 / 14
self.internal_gains_moisture_no_people = 0.5
self.ratio_conv_rad_persons = 0.5
self.machines = 7.0
self.ratio_conv_rad_machines = 0.75
self._use_maintained_illuminance = False # Choose wether lighting power will be given by direct input or calculated by maintained illuminance and lighting_efficiency_lumen
self._lighting_power = 10
self.fixed_lighting_power = 10
self.ratio_conv_rad_lighting = 0.4
self.maintained_illuminance = 500
self.lighting_efficiency_lumen = 100 # lighting efficiency in lm/W_el
self.use_constant_infiltration = False
self.normative_infiltration = 0.5
self.base_infiltration = 0.2
self.max_user_infiltration = 1.0
self.max_overheating_infiltration = [3.0, 2.0]
self.max_summer_infiltration = [1.0, 273.15 + 10, 273.15 + 17]
self.winter_reduction_infiltration = [0.5, 273.15, 273.15 + 10]
self.min_ahu = 0.0
self.max_ahu = 2.6
self.with_ahu = False
self._first_saturday_of_year = 1
self.profiles_weekend_factor = None
self._set_back_times = None
self.heating_set_back = -2
self.cooling_set_back = 2
self._adjusted_opening_times = None
self._with_ideal_thresholds = False
self._heating_profile = [
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
]
self._cooling_profile = [
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
294.15,
]
self._persons_profile = [
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.2,
0.4,
0.6,
0.8,
0.8,
0.4,
0.6,
0.8,
0.8,
0.4,
0.2,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
]
self._machines_profile = [
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
0.2,
0.4,
0.6,
0.8,
0.8,
0.4,
0.6,
0.8,
0.8,
0.4,
0.2,
0.1,
0.1,
0.1,
0.1,
0.1,
0.1,
]
self._lighting_profile = [
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
1.0,
0.0,
0.0,
0.0,
0.0,
0.0,
0.0,
]
self._schedules = None
[docs]
def adjust_profile_by_opening(self, profile):
"""Adjusts the given profile by opening times specified for use
condition with the parameter self.set_back_times.
Parameters
----------
profile : list
list with the given profile (lighting, machines, persons)
"""
new_profile = []
# split profile into daily profiles
profile_len = len(profile)
n_sublists = profile_len // 24
daily_profiles = (profile[i * 24:(i + 1) * 24] for i in
range(n_sublists))
opening_hour_index = self.adjusted_opening_times[0] - 1
closing_hour_index = self.adjusted_opening_times[1] - 1
for profile_day in daily_profiles:
baseload = profile_day[0]
for i, value in enumerate(profile_day):
# check if runtime variable(time) is inside opening times
# +/- delta times
if opening_hour_index <= i <= closing_hour_index:
if value == baseload:
# start new iteration of profile_day from beginning
for j, value2 in enumerate(profile_day):
# search first value which is > baseload
# if
if (
value2 > baseload and i < (
closing_hour_index - opening_hour_index) / 2
):
profile_day[i] = profile_day[j]
break
elif (
value2 > baseload and i >= (
closing_hour_index - opening_hour_index) / 2
):
# value is overwritten every time,
# so that last value that is > baseload
# is used
profile_day[i] = value2
else:
pass
else:
pass
elif not (
opening_hour_index <= i <= closing_hour_index) and \
value != baseload:
# if time is not inside opening times, set value to
# baseload
profile_day[i] = baseload
new_profile.extend(profile_day)
return new_profile
[docs]
def adjust_profile_by_weekend(self, profile):
"""Scales the given profile on weekends. Factor for scaling is taken
from self.profiles_weekend_factor.
Parameters
----------
profile : list
list with the given profile (lighting, machines, persons)
"""
new_profile = []
# check if profile is at least week profile (other cases
# than 24, 168,8760 are excluded already)
if len(profile) == 24:
profile = profile * 7
n_sublists = len(profile) // 24
daily_profiles = (profile[i * 24:(i + 1) * 24] for i in
range(n_sublists))
weekend_days = []
for i in range(self.first_saturday_of_year, 365, 7):
weekend_days.append(i)
weekend_days.append(i + 1)
for day_nr, profile_day in enumerate(daily_profiles, 1):
if day_nr in weekend_days:
profile_day = \
[round((x * self.profiles_weekend_factor), 3)
for x in profile_day]
new_profile.extend(profile_day)
return new_profile
[docs]
def load_use_conditions(self, zone_usage, data_class=None):
"""Load typical use conditions from JSON data base.
Loads Use conditions specified in the JSON.
Parameters
----------
zone_usage : str
code list for zone_usage according to 18599 or self defined
data_class : DataClass()
DataClass containing the bindings for Use Conditions (typically
this is the data class stored in prj.data,
but the user can individually change that. Default is None which
leads to an automatic setter to self.parent.parent.parent.data (
which is DataClass in current project)
"""
if data_class is None:
data_class = self.parent.parent.parent.data
else:
data_class = data_class
usecond_input.load_use_conditions(
use_cond=self, zone_usage=zone_usage, data_class=data_class
)
[docs]
def save_use_conditions(self, data_class=None):
"""Documentation is missing."""
if data_class is None:
data_class = self.parent.parent.parent.data
else:
data_class = data_class
usecond_output.save_use_conditions(use_cond=self, data_class=data_class)
[docs]
@staticmethod
def is_periodic(profile_list):
"""Checks if the given profile list is periodic.
Allowed periods are: 24h, 168h (7 days), 8760h (1year).
Parameters
----------
profile_list: list
given profile as list of hourly values.
"""
if not isinstance(profile_list, list):
profile_list = list(profile_list)
profile_len = len(profile_list)
if profile_len in [24, 168, 8760]:
return True
else:
return False
@property
def persons(self):
return self._persons
@persons.setter
def persons(self, value):
if isinstance(value, OrderedDict):
self._persons = division_from_json(value)
else:
self._persons = value
@property
def with_ideal_thresholds(self):
return self._with_ideal_thresholds
@with_ideal_thresholds.setter
def with_ideal_thresholds(self, value):
if self.with_ahu is False and value is True:
raise ValueError(
"Threshold for ideal heaters should only be used"
" when AHU is used in this zone"
)
else:
self._with_ideal_thresholds = value
@property
def heating_profile(self):
return self._heating_profile
@heating_profile.setter
def heating_profile(self, value):
if not isinstance(value, list):
value = [value] * 24
if self.is_periodic(value):
self._heating_profile = value
else:
raise ValueError(
f"heating profile should be periodic (24h, 168h pr 8760h), "
f"but length is {len(value)}"
)
@property
def cooling_profile(self):
return self._cooling_profile
@cooling_profile.setter
def cooling_profile(self, value):
if not isinstance(value, list):
value = [value] * 24
if self.is_periodic(value):
self._cooling_profile = value
else:
raise ValueError(
f"cooling profile should be periodic (24h, 168h pr 8760h), "
f"but length is {len(value)}"
)
@property
def persons_profile(self):
return self._persons_profile
@persons_profile.setter
def persons_profile(self, value):
if not isinstance(value, list):
value = [value] * 24
if self.is_periodic(value):
self._persons_profile = value
else:
raise ValueError(
f"persons profile should be periodic (24h, 168h pr 8760h), "
f"but length is {len(value)}"
)
@property
def machines_profile(self):
return self._machines_profile
@machines_profile.setter
def machines_profile(self, value):
if not isinstance(value, list):
value = [value] * 24
if self.is_periodic(value):
self._machines_profile = value
else:
raise ValueError(
f"machines profile should be periodic (24h, 168h pr 8760h), "
"but length is {len(value)}"
)
@property
def lighting_profile(self):
return self._lighting_profile
@lighting_profile.setter
def lighting_profile(self, value):
if not isinstance(value, list):
value = [value] * 24
if self.is_periodic(value):
self._lighting_profile = value
else:
raise ValueError(
f"lighting profile should be periodic (24h, 168h pr 8760h), "
"but length is {len(value)}"
)
@property
def schedules(self):
self._schedules = pd.DataFrame(
index=pd.date_range("2019-01-01 00:00:00", periods=8760,
freq="h").to_series().dt.strftime(
"%m-%d %H:%M:%S"),
data={
"heating_profile": list(
islice(cycle(self._heating_profile), 8760)),
"cooling_profile": list(
islice(cycle(self._cooling_profile), 8760)),
"persons_profile": list(
islice(cycle(self._persons_profile), 8760)),
"lighting_profile": list(
islice(cycle(self._lighting_profile), 8760)),
"machines_profile": list(
islice(cycle(self._machines_profile), 8760)),
},
)
return self._schedules
@schedules.setter
def schedules(self, value):
self._schedules = value
[docs]
def calc_adj_schedules(self):
"""calculates adjusted schedules for use conditions. When called the
profiles get adjusted due to specified conditions. Afterwards the
existing schedules will be overwritten by the resulting pandas dataframe
with 8760 h.
"""
if self.adjusted_opening_times:
self._machines_profile = self.adjust_profile_by_opening(
self._machines_profile)
self._lighting_profile = self.adjust_profile_by_opening(
self._lighting_profile)
self._persons_profile = self.adjust_profile_by_opening(
self._persons_profile)
if self.profiles_weekend_factor:
self._machines_profile = self.adjust_profile_by_weekend(
self._machines_profile)
self._lighting_profile = self.adjust_profile_by_weekend(
self._lighting_profile)
self._persons_profile = self.adjust_profile_by_weekend(
self._persons_profile)
if self.set_back_times:
set_back_index_morning, set_back_index_evening = \
self.set_back_times[0] - 1, self.set_back_times[1] - 1
heating_profile, cooling_profile = [], []
for i, value in enumerate(self._heating_profile):
if 0 <= i <= set_back_index_morning \
or set_back_index_evening <= i <= 24:
heating_profile.append(value + self.heating_set_back)
else:
heating_profile.append(value)
self._heating_profile = heating_profile
for i, value in enumerate(self._cooling_profile):
if 0 <= i <= set_back_index_morning \
or set_back_index_evening <= i <= 24:
cooling_profile.append(value + self.cooling_set_back)
else:
cooling_profile.append(value)
self._cooling_profile = cooling_profile
@property
def adjusted_opening_times(self):
return self._adjusted_opening_times
@adjusted_opening_times.setter
def adjusted_opening_times(self, value):
if len(value) != 2:
raise ValueError(f"adjusted_opening_times must be list of length 2,"
f" but list of length {len(value)} was provided")
elif value[0] < 0 or value[0] > 24 or value[1] < 0 or value[1] > 24:
raise ValueError(f"elements of adjusted_opening_times must be "
f"hours between 0 and 24. But are {value[0]} and"
f" {value[1]}")
else:
self._adjusted_opening_times = value
@property
def set_back_times(self):
return self._set_back_times
@set_back_times.setter
def set_back_times(self, value):
if len(value) != 2:
raise ValueError(f"set_back_times must be list of length 2,"
f" but list of length {len(value)} was provided")
elif value[0] < 0 or value[0] > 24 or value[1] < 0 or value[1] > 24:
raise ValueError(f"elements of set_back_times must be "
f"hours between 0 and 24. But are {value[0]} and"
f" {value[1]}")
else:
self._set_back_times = value
@property
def first_saturday_of_year(self):
return self._first_saturday_of_year
@first_saturday_of_year.setter
def first_saturday_of_year(self, value):
if value < 1 or value > 7:
raise ValueError(f"first_saturday_of_year must be int between "
f"[1, 7] but is {value}")
elif not isinstance(value, int):
raise ValueError(f"first_saturday_of_year must be int but is "
f"{type(value)}")
else:
self._first_saturday_of_year = value
@property
def parent(self):
return self._parent
@parent.setter
def parent(self, value):
if value is not None:
ass_error_1 = "Parent has to be an instance of ThermalZone()"
assert type(value).__name__ == "ThermalZone", ass_error_1
self._parent = value
self._parent._use_conditions = self
else:
self._parent = None
@property
def use_maintained_illuminance(self):
return self._use_maintained_illuminance
@use_maintained_illuminance.setter
def use_maintained_illuminance(self, value):
if value:
self._lighting_power = self.maintained_illuminance / self.lighting_efficiency_lumen
else:
self._lighting_power = self.fixed_lighting_power
self._use_maintained_illuminance = value
@property
def lighting_power(self):
return self._lighting_power
@lighting_power.setter
def lighting_power(self, value):
if self.use_maintained_illuminance:
warnings.warn(
"Parameter 'use_maintained_illuminance' is 'True'!\n"
"Parameter 'lighting_power' will be overwritten and 'use_maintained_illuminance' will be set to 'False'.",
)
self._use_maintained_illuminance = False
self._lighting_power = value
@property
def infiltration_rate(self):
warnings.warn(
"'infiltration_rate' is deprecated and will be removed in a future release. "
"Use 'base_infiltration' instead.",
DeprecationWarning,
stacklevel=2)
return self.base_infiltration
@infiltration_rate.setter
def infiltration_rate(self, value):
self.base_infiltration = value
warnings.warn(
"'infiltration_rate' is deprecated and will be removed in a future release. "
"Use 'base_infiltration' instead.",
DeprecationWarning,
stacklevel=2)