# coding=utf-8
"""Complete definition of ventilation in a simulation, including schedule and load."""
from __future__ import division
from honeybee._lockable import lockable
from honeybee.typing import float_positive, float_in_range, int_positive
from ._base import _LoadBase
from ..schedule.ruleset import ScheduleRuleset
from ..schedule.fixedinterval import ScheduleFixedInterval
from ..units import convert_ventilation_flow_per_area, \
convert_ventilation_flow_per_zone, convert_pressure_rise
from ..lib.schedules import always_on
import honeybee_energy.lib.scheduletypelimits as _type_lib
[docs]
@lockable
class ExhaustAir(_LoadBase):
"""A complete definition of exhaust air, including schedules and flow rates.
Note the the 2 ventilation types (flow_per_area and flow_per_fixture) are
ultimately added together to yield the final exhaust air flow rate used
in the simulation.
Args:
identifier: Text string for a unique ExhaustAir ID. Must be < 100 characters
and not contain any EnergyPlus special characters. This will be used to
identify the object across a model and in the exported IDF.
flow_per_area: A numerical value for the intensity of exhaust air ventilation
in m3/s per square meter of floor area. (Default: 0).
flow_per_fixture: A numerical value for the level of exhaust air ventilation
in m3/s for each fixture in the room. The term "fixture" is used
broadly as a way to reference a wide variety of contaminant sources
such as toilets/urinals, shower heads, kitchen hoods, fume hoods,
etc. (Default: 0).
fixture_count: An integer for the number of fixtures in the room. This
is multiplied by the flow_per_fixture, which is then added to the
flow_per_area to yield the final exhaust air flow rate (Default: 1).
schedule: An optional ScheduleRuleset or ScheduleFixedInterval for the
exhaust air ventilation over the course of the year. The type of this
schedule should be Fractional and the fractional values get multiplied by
the total design flow rate to yield a complete exhaust air profile.
Values of 0 in the schedule will shut the exhaust fan off completely. If None,
the design level of exhaust air will be used throughout all timesteps
of the simulation, meaning that this schedule is Always On. (Default: None).
pressure_rise: A number for the the pressure rise across the fan in Pascals
(N/m2). This is often a function of the fan speed and the conditions in
which the fan is operating. It plays an important role in determining
the amount of energy consumed by the fan. Typical kitchen and bathroom
exhaust fans have pressure rises around 125 Pa but, in healthcare
settings where filters create more resistance, higher pressures
around 250 Pa are more common. (Default: 125).
efficiency: A number between 0 and 1 for the overall efficiency of the fan.
Specifically, this is the ratio of the power delivered to the fluid
to the electrical input power. It is the product of the fan motor
efficiency and the fan impeller efficiency.
Fans that have a higher blade diameter, no obstructions or filters,
and operate at lower speeds with smaller pressure rises for
their size tend to have higher efficiencies. Because motor efficiencies
are typically between 0.8 and 0.9, the best overall fan efficiencies
tend to be around 0.7 with most typical fan efficiencies between 0.5 and
0.7. When filters are added, which is common for most exhaust fans,
the total efficiency typically ends up between 0.3 and 0.4. (Default: 0.35).
balancing_schedule: An optional ScheduleRuleset or ScheduleFixedInterval
for the fraction of exhaust air that is unbalanced by simple airflows,
such as infiltration, natural ventilation, or zone mixing. Unbalanced
exhaust is modeled as being provided by the outdoor air system in the
central air system such that values of 1 in this schedule indicate
all exhaust air balancing done by the mechanical system and values of 0
indicate all air balanced by simple air flows. If None, then
all the exhaust air flow is assumed to be unbalanced by simple
airflows. The the flow rates at the zone return air node are reduced
by the flow rate that is being exhausted and the zone outdoor air
controller will ensure that the outdoor air flow rate is sufficient
to serve the exhaust. (Default: None).
Properties:
* identifier
* display_name
* flow_per_area
* flow_per_fixture
* fixture_count
* schedule
* pressure_rise
* efficiency
* balancing_schedule
* user_data
"""
__slots__ = (
'_flow_per_area', '_flow_per_fixture', '_fixture_count', '_schedule',
'_pressure_rise', '_efficiency', '_balancing_schedule'
)
def __init__(
self, identifier, flow_per_area=0, flow_per_fixture=0, fixture_count=1,
schedule=None, pressure_rise=125, efficiency=0.35, balancing_schedule=None
):
"""Initialize ExhaustAir."""
_LoadBase.__init__(self, identifier)
self.flow_per_area = flow_per_area
self.flow_per_fixture = flow_per_fixture
self.fixture_count = fixture_count
self.schedule = schedule
self.pressure_rise = pressure_rise
self.efficiency = efficiency
self.balancing_schedule = balancing_schedule
@property
def flow_per_area(self):
"""Get or set the exhaust ventilation in m3/s per square meter of floor area."""
return self._flow_per_area
@flow_per_area.setter
def flow_per_area(self, value):
self._flow_per_area = float_positive(value, 'exhaust flow per area') if \
value is not None else 0
@property
def flow_per_fixture(self):
"""Get or set the exhaust ventilation in m3/s per fixture."""
return self._flow_per_fixture
@flow_per_fixture.setter
def flow_per_fixture(self, value):
self._flow_per_fixture = float_positive(value, 'exhaust flow per fixture') if \
value is not None else 0
@property
def fixture_count(self):
"""Get or set an integer for the number of fixtures in the room."""
return self._fixture_count
@fixture_count.setter
def fixture_count(self, value):
self._fixture_count = \
int_positive(value, 'exhaust air fixture count') if \
value is not None else 1
@property
def schedule(self):
"""Get or set a ScheduleRuleset or ScheduleFixedInterval for exhaust air."""
return self._schedule if self._schedule is not None else always_on
@schedule.setter
def schedule(self, value):
if value is not None:
assert isinstance(value, (ScheduleRuleset, ScheduleFixedInterval)), \
'Expected ScheduleRuleset or ScheduleFixedInterval for ExhaustAir ' \
'schedule. Got {}.'.format(type(value))
self._check_fractional_schedule_type(value, 'ExhaustAir')
value.lock() # lock editing in case schedule has multiple references
self._schedule = value
@property
def pressure_rise(self):
"""Get or set a number for the fan pressure rise in Pa."""
if self._pressure_rise is not None:
return self._pressure_rise
return self._default_pressure_rise()
@pressure_rise.setter
def pressure_rise(self, value):
if value is not None:
value = float_positive(value, 'fan pressure rise')
self._pressure_rise = value
@property
def efficiency(self):
"""Get or set a number between 0 and 1 for the fan efficiency."""
if self._efficiency is not None:
return self._efficiency
return self._default_efficiency()
@efficiency.setter
def efficiency(self, value):
if value is not None:
value = float_in_range(value, 0, 1, 'fan efficiency')
self._efficiency = value
@property
def balancing_schedule(self):
"""Get or set a ScheduleRuleset or ScheduleFixedInterval for the unbalanced air fraction.
"""
return self._balancing_schedule if self._balancing_schedule is not None else always_on
@balancing_schedule.setter
def balancing_schedule(self, value):
if value is not None:
assert isinstance(value, (ScheduleRuleset, ScheduleFixedInterval)), \
'Expected ScheduleRuleset or ScheduleFixedInterval for ExhaustAir ' \
'balancing_schedule. Got {}.'.format(type(value))
self._check_fractional_schedule_type(value, 'ExhaustAir')
value.lock() # lock editing in case schedule has multiple references
self._balancing_schedule = value
@property
def flow_per_area_si(self):
"""Get the flow_per_area in the standard SI unit of L/s/m2."""
return convert_ventilation_flow_per_area(self.flow_per_area, 'si')
@property
def flow_per_area_ip(self):
"""Get the flow_per_area in the standard IP unit of cfm/ft2."""
return convert_ventilation_flow_per_area(self.flow_per_area, 'ip')
@property
def flow_per_fixture_si(self):
"""Get the flow_per_fixture in the standard SI unit of L/s."""
return convert_ventilation_flow_per_zone(self.flow_per_fixture, 'si')
@property
def flow_per_fixture_ip(self):
"""Get the flow_per_fixture in the standard IP unit of cfm."""
return convert_ventilation_flow_per_zone(self.flow_per_fixture, 'ip')
@property
def pressure_rise_si(self):
"""Get the pressure_rise in the standard SI unit of Pa."""
return convert_pressure_rise(self.pressure_rise, 'si')
@property
def pressure_rise_ip(self):
"""Get the pressure_rise in the standard IP unit of inches of H2O."""
return convert_pressure_rise(self.pressure_rise, 'ip')
[docs]
def room_absolute_flow(self, room):
"""Get the total flow rate of exhaust ventilation air for a Room in m3/s.
The result of this method accounts for both ways of specifying exhaust air.
Args:
room: The honeybee Room to which the ventilation object is assigned.
"""
total_flows = [self.flow_per_fixture * self.fixture_count]
if self.flow_per_area != 0:
total_flows.append(self.flow_per_area * room.floor_area)
return sum(total_flows)
[docs]
@classmethod
def from_dict(cls, data, schedules=None):
"""Create a ExhaustAir object from a dictionary.
Note that the dictionary must be a non-abridged version for this classmethod
to work.
Args:
data: A ExhaustAir dictionary in following the format below.
schedules: Optional dictionary with schedule identifiers as keys and
honeybee schedule objects as values (either ScheduleRuleset or
ScheduleFixedInterval). When specified, these will be prioritized
over the child objects underneath their unabridged specification.
.. code-block:: python
{
"type": 'ExhaustAir',
"identifier": 'Bathroom_ExhaustAir_000050_001_1',
"display_name": 'Bathroom ExhaustAir',
"flow_per_area": 0.0005, # flow per square meter of floor area
"flow_per_fixture": 0.01, # flow per fixture
"fixture_count": 1, # number of fixtures in the room
"schedule": {}, # ScheduleRuleset/ScheduleFixedInterval dictionary
"pressure_rise": 125, # fan pressure rise in Pa
"efficiency": 0.35 # fan efficiency
"balancing_schedule": {}, # ScheduleRuleset/ScheduleFixedInterval dictionary
}
"""
assert data['type'] == 'ExhaustAir', \
'Expected ExhaustAir dictionary. Got {}.'.format(data['type'])
area, fix_flow, fix_count, press, eff = cls._optional_dict_keys(data)
sched = cls._get_schedule_from_dict(data['schedule'], schedules) \
if 'schedule' in data and data['schedule'] is not None else None
b_sched = cls._get_schedule_from_dict(data['balancing_schedule'], schedules) \
if 'balancing_schedule' in data and data['balancing_schedule'] is not None \
else None
new_obj = cls(
data['identifier'], area, fix_flow, fix_count, sched, press, eff, b_sched
)
if 'display_name' in data and data['display_name'] is not None:
new_obj.display_name = data['display_name']
if 'user_data' in data and data['user_data'] is not None:
new_obj.user_data = data['user_data']
if 'properties' in data and data['properties'] is not None:
new_obj.properties._load_extension_attr_from_dict(data['properties'])
return new_obj
[docs]
@classmethod
def from_dict_abridged(cls, data, schedule_dict):
"""Create a ExhaustAir object from an abridged dictionary.
Args:
data: A ExhaustAirAbridged dictionary in following the format below.
schedule_dict: A dictionary with schedule identifiers as keys and
honeybee schedule objects as values (either ScheduleRuleset or
ScheduleFixedInterval). These will be used to assign the schedules
to the ExhaustAir object.
.. code-block:: python
{
"type": 'ExhaustAirAbridged',
"identifier": 'Bathroom_ExhaustAir_000050_001_1',
"display_name": 'Bathroom ExhaustAir',
"flow_per_area": 0.0005, # flow per square meter of floor area
"flow_per_fixture": 0.01, # flow per fixture
"fixture_count": 1, # number of fixtures in the room
"schedule": "Bathroom ExhaustAir Schedule", # Schedule identifier
"pressure_rise": 125, # fan pressure rise in Pa
"efficiency": 0.35, # fan efficiency
"balancing_schedule": "BR Makeup ExhaustAir Schedule", # Schedule identifier
}
"""
assert data['type'] == 'ExhaustAirAbridged', \
'Expected ExhaustAirAbridged dictionary. Got {}.'.format(data['type'])
area, fix_flow, fix_count, press, eff = cls._optional_dict_keys(data)
sched = None
if 'schedule' in data and data['schedule'] is not None:
try:
sched = schedule_dict[data['schedule']]
except KeyError as e:
raise ValueError('Failed to find {} in the schedule_dict.'.format(e))
b_sched = None
if 'balancing_schedule' in data and data['balancing_schedule'] is not None:
try:
b_sched = schedule_dict[data['balancing_schedule']]
except KeyError as e:
raise ValueError('Failed to find {} in the schedule_dict.'.format(e))
new_obj = cls(
data['identifier'], area, fix_flow, fix_count, sched, press, eff, b_sched
)
if 'display_name' in data and data['display_name'] is not None:
new_obj.display_name = data['display_name']
if 'user_data' in data and data['user_data'] is not None:
new_obj.user_data = data['user_data']
if 'properties' in data and data['properties'] is not None:
new_obj.properties._load_extension_attr_from_dict(data['properties'])
return new_obj
[docs]
def to_dict(self, abridged=False):
"""ExhaustAir dictionary representation.
Args:
abridged: Boolean to note whether the full dictionary describing the
object should be returned (False) or just an abridged version (True),
which only specifies the identifiers of schedules. (Default: False).
"""
base = {'type': 'ExhaustAir'} if not abridged \
else {'type': 'ExhaustAirAbridged'}
base['identifier'] = self.identifier
if self.flow_per_area != 0:
base['flow_per_area'] = self.flow_per_area
if self.flow_per_fixture != 0:
base['flow_per_fixture'] = self.flow_per_fixture
if self.fixture_count != 1:
base['fixture_count'] = self.fixture_count
if self._schedule is not None:
base['schedule'] = self.schedule.to_dict() if not \
abridged else self.schedule.identifier
if self.pressure_rise != 125:
base['pressure_rise'] = self.pressure_rise
if self.efficiency != 0:
base['efficiency'] = self.efficiency
if self._balancing_schedule is not None:
base['balancing_schedule'] = self.balancing_schedule.to_dict() if not \
abridged else self.balancing_schedule.identifier
if self._display_name is not None:
base['display_name'] = self.display_name
if self._user_data is not None:
base['user_data'] = self.user_data
return base
[docs]
@staticmethod
def average(identifier, exhaust_airs, weights=None, timestep_resolution=1):
"""Get a ExhaustAir object that's an average between other ExhaustAirs.
Args:
identifier: Text string for a unique ID for the new averaged ExhaustAir.
Must be < 100 characters and not contain any EnergyPlus special
characters. This will be used to identify the object across a model
and in the exported IDF.
exhaust_airs: A list of ExhaustAir objects that will be averaged
together to make a new ExhaustAir.
weights: An optional list of fractional numbers with the same length
as the input exhaust_airs. These will be used to weight each of the
ExhaustAir objects in the resulting average. Note that these weights
can sum to less than 1 in which case the average flow rates
will assume 0 for the unaccounted fraction of the weights.
timestep_resolution: An optional integer for the timestep resolution
at which the schedules will be averaged. Any schedule details
smaller than this timestep will be lost in the averaging
process. (Default: 1).
"""
weights, u_weights = \
ExhaustAir._check_avg_weights(exhaust_airs, weights, 'ExhaustAir')
# calculate the average values
area = sum([vent.flow_per_area * w
for vent, w in zip(exhaust_airs, weights)])
fixture = sum([vent.flow_per_fixture * vent.fixture_count
for vent in exhaust_airs])
press = sum([vent.pressure_rise * w
for vent, w in zip(exhaust_airs, weights)])
eff = sum([vent.efficiency * w
for vent, w in zip(exhaust_airs, weights)])
# round the effectiveness terms to avoid tolerance issues
area = round(area, 6)
press = round(press, 3)
eff = round(eff, 3)
# calculate the average schedules
scheds = [vent._schedule for vent in exhaust_airs]
if all(val is None for val in scheds):
sched = None
else:
full_vent = ScheduleRuleset.from_constant_value(
'Full ExhaustAir', 1, _type_lib.fractional)
for i, sch in enumerate(scheds):
if sch is None:
scheds[i] = full_vent
sched = ExhaustAir._average_schedule(
'{} Schedule'.format(identifier), scheds, u_weights, timestep_resolution)
b_scheds = [vent._balancing_schedule for vent in exhaust_airs]
if all(val is None for val in b_scheds):
b_sched = None
else:
full_vent = ScheduleRuleset.from_constant_value(
'Full ExhaustAir', 1, _type_lib.fractional)
for i, sch in enumerate(b_scheds):
if sch is None:
b_scheds[i] = full_vent
b_sched = ExhaustAir._average_schedule(
'{} Schedule'.format(identifier), b_scheds, u_weights, timestep_resolution)
# return the averaged object
return ExhaustAir(identifier, area, fixture, 1, sched, press, eff, b_sched)
[docs]
@staticmethod
def combine_room_exhaust_airs(identifier, rooms, timestep_resolution=1):
"""Get a ExhaustAir object that represents the sum across rooms.
In this process of combining exhaust air requirements, the following
rules hold: 1. Flow per floor area gets recomputed using the floor
areas of each room. 2. Flow defined by fixtures it totaled across
all of the input rooms and then assigned to the result as a single
flow value for one fixture.
In the case of exhaust air schedules, the strictest schedule governs and
note that the absence of a exhaust air schedule means the schedule is
Always On. So, if one room has a exhaust air schedule and the other
does not, then the schedule essentially gets removed. If each room has
a different exhaust air schedule, then a new schedule will be created
using the maximum value across the two schedules at each timestep.
Args:
identifier: Text string for a unique ID for the new ExhaustAir object.
Must be < 100 characters and not contain any EnergyPlus special
characters. This will be used to identify the object across a model
and in the exported IDF.
rooms: A list of Rooms that will have their ExhaustAir objects
combined to make a new ExhaustAir.
timestep_resolution: An optional integer for the timestep resolution at
which conflicting ventilation schedules will be resolved. (Default: 1).
"""
# compute weights based on floor areas and volumes
exhaust_airs, floor_areas, scheds, b_scheds = [], [], [], []
for room in rooms:
if room.properties.energy.exhaust_air is None:
exhaust_airs.append(ExhaustAir('dummy_ea'))
else:
exhaust_airs.append(room.properties.energy.exhaust_air)
scheds.append(room.properties.energy.ventilation._schedule)
b_scheds.append(room.properties.energy.ventilation._balancing_schedule)
floor_areas.append(room.floor_area)
total_floor = sum(floor_areas)
floor_weights = [ar / total_floor for ar in floor_areas]
# calculate the average values
area = sum([vent.flow_per_area * w
for vent, w in zip(exhaust_airs, floor_weights)])
fixture = sum(vent.flow_per_fixture * vent.fixture_count for vent in exhaust_airs)
press = sum([vent.pressure_rise * w
for vent, w in zip(exhaust_airs, floor_weights)])
eff = sum([vent.efficiency * w
for vent, w in zip(exhaust_airs, floor_weights)])
# calculate the average schedules
if len(scheds) == 0 or any(val is None for val in scheds):
sched = None
else:
base_sch = scheds[0]
if all(sch is base_sch for sch in scheds) or len(set(scheds)) == 1:
sched = scheds[0]
else:
sched = ExhaustAir._max_schedule(
'{} Schedule'.format(identifier), scheds, timestep_resolution)
if all(val is None for val in b_scheds):
b_sched = None
else:
base_sch = b_scheds[0]
if all(sch is base_sch for sch in b_scheds) or len(set(b_scheds)) == 1:
b_sched = b_scheds[0]
else:
b_sched = ExhaustAir._max_schedule(
'{} Schedule'.format(identifier), b_scheds, timestep_resolution)
# return the averaged object
return ExhaustAir(identifier, area, fixture, 1, sched, press, eff, b_sched)
@staticmethod
def _optional_dict_keys(data):
"""Get the optional keys from an ExhaustAir dictionary."""
area = data['flow_per_area'] if 'flow_per_area' in data else 0
flow_per_fixture = data['flow_per_fixture'] if 'flow_per_fixture' in data else 0
fixture_count = data['fixture_count'] if 'fixture_count' in data else 1
press = data['pressure_rise'] if 'pressure_rise' in data else 125
eff = data['efficiency'] if 'efficiency' in data else 0.35
return area, flow_per_fixture, fixture_count, press, eff
def __key(self):
"""A tuple based on the object properties, useful for hashing."""
return (
self.identifier, self.flow_per_area, self.flow_per_fixture,
self.fixture_count, hash(self.schedule),
self.pressure_rise, self.efficiency, hash(self.balancing_schedule)
)
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return isinstance(other, ExhaustAir) and self.__key() == other.__key()
def __ne__(self, other):
return not self.__eq__(other)
def __copy__(self):
new_obj = ExhaustAir(
self._identifier, self._flow_per_area, self._flow_per_fixture,
self._fixture_count, self._schedule,
self._pressure_rise, self.efficiency, self.balancing_schedule
)
new_obj._display_name = self._display_name
new_obj._user_data = None if self._user_data is None else self._user_data.copy()
return new_obj
def __repr__(self):
return 'ExhaustAir: {} [{} m3/s-m2] [{} m3/fixture]'.format(
self.display_name, round(self.flow_per_area, 6),
round(self.flow_per_fixture, 3)
)