# coding=utf-8
"""Detailed HVAC system object defined using IronBug or OpenStudio .NET bindings."""
from __future__ import division
from honeybee._lockable import lockable
from ._base import _HVACSystem
from .idealair import IdealAirSystem
[docs]
@lockable
class DetailedHVAC(_HVACSystem):
"""Detailed HVAC system object defined using IronBug or OpenStudio .NET bindings.
Args:
identifier: Text string for detailed system identifier. Must be < 100 characters
and not contain any EnergyPlus special characters.
specification: A JSON-serializable dictionary representing the full
specification of the detailed system. This can be obtained by calling
the ToJson() method on any IronBug HVAC system and then serializing
the resulting JSON string into a Python dictionary using the native
Python json package. Note that the Rooms that the HVAC is assigned to
must be specified as ThermalZones under this specification in order
for the resulting Model this HVAC is a part of to be valid.
Properties:
* identifier
* specification
* thermal_zones
* design_type
* air_loop_count
* economizer_type
* sensible_heat_recovery
* latent_heat_recovery
* display_name
* user_data
"""
NO_AIR_LOOP = 'Ironbug.HVAC.IB_NoAirLoop'
AIR_LOOP = 'Ironbug.HVAC.IB_AirLoopHVAC'
BRANCHES = 'Ironbug.HVAC.IB_AirLoopBranches'
OA_SYSTEM = 'Ironbug.HVAC.IB_OutdoorAirSystem'
OA_CONTROLLER = 'Ironbug.HVAC.IB_ControllerOutdoorAir'
HEAT_RECOVERY = 'Ironbug.HVAC.IB_HeatExchangerAirToAirSensibleAndLatent'
HR_SENSIBLE = (
'SensibleEffectivenessat75CoolingAirFlow',
'SensibleEffectivenessat75HeatingAirFlow'
)
HR_LATENT = (
'LatentEffectivenessat75CoolingAirFlow',
'LatentEffectivenessat75HeatingAirFlow'
)
ECONOMIZER_TYPES = ('NoEconomizer', 'DifferentialDryBulb', 'DifferentialEnthalpy',
'DifferentialDryBulbAndEnthalpy', 'FixedDryBulb',
'FixedEnthalpy', 'ElectronicEnthalpy')
__slots__ = ('_specification', '_thermal_zones', '_design_type', '_air_loop_count',
'_economizer_type', '_sensible_heat_recovery', '_latent_heat_recovery')
def __init__(self, identifier, specification):
"""Initialize DetailedHVAC."""
# initialize base HVAC system properties
_HVACSystem.__init__(self, identifier)
self.specification = specification
@property
def specification(self):
"""Get or set a dictionary for the full specification of this HVAC.
This can be obtained by calling the SaveAsJson() method on any IronBug HVAC
system and then serializing the resulting JSON string into a Python dictionary
using the native Python json package.
"""
return self._specification
@specification.setter
def specification(self, value):
assert isinstance(value, dict), 'Expected dictionary for DetailedHVAC' \
'object specification. Got {}.'.format(type(value))
thermal_zones, design_type, air_loop_count = [], 'HeatCool', 0
econ_type, sensible_hr, latent_hr = 'NoEconomizer', 0, 0
try:
for a_loop in value['AirLoops']:
if a_loop['$type'].startswith(self.NO_AIR_LOOP):
# get all of the zones on the demand side
for zone in a_loop['ThermalZones']:
for z_attr in zone['CustomAttributes']:
if z_attr['Field']['FullName'] == 'Name':
thermal_zones.append(z_attr['Value'])
elif a_loop['$type'].startswith(self.AIR_LOOP):
# determine whether it's an AllAir system or DOAS system
air_loop_count += 1
design_type = 'AllAir'
if 'SizingSystem' in a_loop and \
'CustomAttributes' in a_loop['SizingSystem']:
for sz_attr in a_loop['SizingSystem']['CustomAttributes']:
if sz_attr['Field']['FullName'] == 'TypeofLoadtoSizeOn':
if sz_attr['Value'] == 'VentilationRequirement':
design_type = 'DOAS'
# determine the type of economizer or heat recovery
for comp in a_loop['SupplyComponents']:
if comp['$type'].startswith(self.OA_SYSTEM):
for child in comp['Children']:
if child['$type'].startswith(self.OA_CONTROLLER):
if 'CustomAttributes' in child:
for attr in child['CustomAttributes']:
f_name = attr['Field']['FullName']
if f_name == 'EconomizerControlType':
econ_type = self._f_econ(attr['Value'])
if 'IBProperties' in comp and \
'OAStreamObjs' in comp['IBProperties']:
for oa_comp in comp['IBProperties']['OAStreamObjs']:
if oa_comp['$type'].startswith(self.HEAT_RECOVERY):
if 'CustomAttributes' in oa_comp:
for oa_attr in oa_comp['CustomAttributes']:
f_name = oa_attr['Field']['FullName']
if f_name in self.HR_SENSIBLE:
sensible_hr = oa_attr['Value']
if f_name in self.HR_LATENT:
latent_hr = oa_attr['Value']
# get all of the zones on the demand side
for comp in a_loop['DemandComponents']:
if comp['$type'].startswith(self.BRANCHES):
for branch in comp['Branches']:
for z_attr in branch[0]['CustomAttributes']:
if z_attr['Field']['FullName'] == 'Name':
thermal_zones.append(z_attr['Value'])
else:
raise ValueError('DetailedHVAC specification does not contain '
'any ThermalZones that can be matched to Rooms.')
except KeyError as e:
raise ValueError('DetailedHVAC specification is not valid:\n{}'.format(e))
self._thermal_zones = tuple(thermal_zones)
self._design_type = design_type
self._air_loop_count = air_loop_count
self._economizer_type = econ_type
self._sensible_heat_recovery = sensible_hr
self._latent_heat_recovery = latent_hr
self._specification = value
@property
def thermal_zones(self):
"""Get a tuple of strings for the Rooms/Zones to which the HVAC is assigned."""
return self._thermal_zones
@property
def design_type(self):
"""Text for the structure of the system. It will be one of the following.
* AllAir
* DOAS
* HeatCool
"""
return self._design_type
@property
def air_loop_count(self):
"""Get an integer for the number of air loops in the system."""
return self._air_loop_count
@property
def economizer_type(self):
"""Get text to indicate the type of air-side economizer.
Choose from the following options.
* NoEconomizer
* DifferentialDryBulb
* DifferentialEnthalpy
* DifferentialDryBulbAndEnthalpy
* FixedDryBulb
* FixedEnthalpy
* ElectronicEnthalpy
"""
return self._economizer_type
@property
def sensible_heat_recovery(self):
"""Get a number for the effectiveness of sensible heat recovery."""
return self._sensible_heat_recovery
@property
def latent_heat_recovery(self):
"""Get a number for the effectiveness of latent heat recovery."""
return self._latent_heat_recovery
[docs]
def sync_room_ids(self, room_map):
"""Sync this DetailedHVAC with Rooms that had their IDs changed.
This is useful after running the Model.reset_ids() command to ensure that
the bi-directional Room references between DetailedHVAC and Honeybee Rooms
is correct.
Args:
room_map: A dictionary that relates the original Rooms identifiers (keys)
to the new identifiers (values) of the Rooms in the Model.
"""
thermal_zones, air_loop_count = [], 0
hvac_spec = self._specification
for a_loop in hvac_spec['AirLoops']:
if a_loop['$type'].startswith(self.NO_AIR_LOOP):
for zone in a_loop['ThermalZones']:
for z_attr in zone['CustomAttributes']:
if z_attr['Field']['FullName'] == 'Name':
z_attr['Value'] = room_map[z_attr['Value']]
thermal_zones.append(z_attr['Value'])
elif a_loop['$type'].startswith(self.AIR_LOOP):
air_loop_count += 1
for comp in a_loop['DemandComponents']:
if comp['$type'].startswith(self.BRANCHES):
for branch in comp['Branches']:
for z_attr in branch[0]['CustomAttributes']:
if z_attr['Field']['FullName'] == 'Name':
z_attr['Value'] = room_map[z_attr['Value']]
thermal_zones.append(z_attr['Value'])
# unlock object and set attributes
was_locked = False
if self._locked:
was_locked = True
self.unlock()
self._air_loop_count = air_loop_count
self._thermal_zones = tuple(thermal_zones)
self._specification = hvac_spec
if was_locked: # set the object back to being locked
self.lock()
[docs]
def to_ideal_air_equivalent(self):
"""This method is NOT YET IMPLEMENTED."""
econ_typ = self.economizer_type
if econ_typ not in self.ECONOMIZER_TYPES[:3]:
enth_types = ('FixedEnthalpy', 'ElectronicEnthalpy')
econ_typ = 'DifferentialEnthalpy' if econ_typ in enth_types \
else 'DifferentialDryBulb'
i_sys = IdealAirSystem(
self.identifier, economizer_type=econ_typ,
sensible_heat_recovery=self.sensible_heat_recovery,
latent_heat_recovery=self.latent_heat_recovery)
i_sys._display_name = self._display_name
return i_sys
[docs]
@classmethod
def from_dict(cls, data):
"""Create a HVAC object from a dictionary.
Args:
data: A HVAC dictionary in following the format below.
.. code-block:: python
{
"type": "DetailedHVAC",
"identifier": "Classroom1_System", # identifier for the HVAC
"display_name": "Custom VAV System", # name for the HVAC
"specification": {} # dictionary for the full HVAC specification
}
"""
assert data['type'] == 'DetailedHVAC', \
'Expected {} dictionary. Got {}.'.format('DetailedHVAC', data['type'])
new_obj = cls(data['identifier'], data['specification'])
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']
return new_obj
[docs]
@classmethod
def from_dict_abridged(cls, data, schedule_dict):
"""Create a HVAC object from an abridged dictionary.
Args:
data: An abridged dictionary in following the format below.
schedule_dict: A dictionary with schedule identifiers as keys and honeybee
schedule objects as values.
.. code-block:: python
{
"type": "DetailedHVAC",
"identifier": "Classroom1_System", # identifier for the HVAC
"display_name": "Custom VAV System", # name for the HVAC
"specification": {} # dictionary for the full HVAC specification
}
"""
# this is the same as the from_dict method for as long as there are not schedules
return cls.from_dict(data)
[docs]
def to_dict(self, abridged=False):
"""DetailedHVAC dictionary representation.
Args:
abridged: Boolean to note whether the full dictionary describing the
object should be returned (False) or just an abridged version (True).
This input currently has no effect but may eventually have one if
schedule-type properties are exposed on this object.
"""
base = {'type': 'DetailedHVAC'}
base['identifier'] = self.identifier
base['specification'] = self.specification
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
def _f_econ(self, value):
clean_input = value.lower()
for key in self.ECONOMIZER_TYPES:
if key.lower() == clean_input:
value = key
break
else:
raise ValueError(
'economizer_type {} is not recognized.\nChoose from the '
'following:\n{}'.format(value, self.ECONOMIZER_TYPES))
return value
def __copy__(self):
new_obj = self.__class__(self.identifier, self.specification.copy())
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 __key(self):
"""A tuple based on the object properties, useful for hashing."""
return (self._identifier, self._air_loop_count) + self._thermal_zones
def __hash__(self):
return hash(self.__key())
def __eq__(self, other):
return isinstance(other, self.__class__) and self.__key() == other.__key()
def __ne__(self, other):
return not self.__eq__(other)
def __repr__(self):
return 'DetailedHVAC: {} [air loops: {}] [zones: {}]'.format(
self.display_name, self.air_loop_count, len(self.thermal_zones))