diff --git a/.coverage b/.coverage index 3541b50..5eb2680 100644 Binary files a/.coverage and b/.coverage differ diff --git a/honeybee_energy_ph/_extend_honeybee_energy_ph.py b/honeybee_energy_ph/_extend_honeybee_energy_ph.py index 853460e..8e8f74a 100644 --- a/honeybee_energy_ph/_extend_honeybee_energy_ph.py +++ b/honeybee_energy_ph/_extend_honeybee_energy_ph.py @@ -24,6 +24,7 @@ ServiceHotWaterProperties, WindowConstructionProperties, WindowConstructionShadeProperties, + ProcessProperties, ) from honeybee_energy.schedule.ruleset import ScheduleRulesetProperties @@ -35,6 +36,7 @@ from honeybee_energy_ph.properties.load.equipment import ElectricEquipmentPhProperties from honeybee_energy_ph.properties.load.lighting import LightingPhProperties from honeybee_energy_ph.properties.load.people import PeoplePhProperties +from honeybee_energy_ph.properties.load.process import ProcessPhProperties from honeybee_energy_ph.properties.materials.opaque import ( EnergyMaterialNoMassPhProperties, EnergyMaterialPhProperties, @@ -67,7 +69,7 @@ setattr(ElectricEquipmentProperties, "_ph", None) setattr(PeopleProperties, "_ph", None) setattr(LightingProperties, "_ph", None) - +setattr(ProcessProperties, "_ph", None) # ----------------------------------------------------------------------------- @@ -153,6 +155,11 @@ def lighting_ph_properties(self): return self._ph +def process_ph_properties(self): + if self._ph is None: + self._ph = ProcessPhProperties(self.host) + return self._ph + # ----------------------------------------------------------------------------- # Step 3) @@ -178,3 +185,4 @@ def lighting_ph_properties(self): setattr(ElectricEquipmentProperties, "ph", property(elec_equip_ph_properties)) setattr(PeopleProperties, "ph", property(people_ph_properties)) setattr(LightingProperties, "ph", property(lighting_ph_properties)) +setattr(ProcessProperties, "ph", property(process_ph_properties)) \ No newline at end of file diff --git a/honeybee_energy_ph/load/_ph_equip_types.py b/honeybee_energy_ph/load/_ph_equip_types.py new file mode 100644 index 0000000..1a50fd1 --- /dev/null +++ b/honeybee_energy_ph/load/_ph_equip_types.py @@ -0,0 +1,63 @@ +# -*- Python Version: 2.7 -*- +# -*- coding: utf-8 -*- + +"""HB-PH Electric Equipment Types.""" + +try: + from honeybee_ph_utils import enumerables +except ImportError as e: + raise ImportError("Failed to import honeybee_ph_utils: {}".format(e)) + + +# ----------------------------------------------------------------------------- +# - Type Enums + + +class PhDishwasherType(enumerables.CustomEnum): + allowed = [ + "1-DHW CONNECTION", + "2-COLD WATER CONNECTION", + ] + + def __init__(self, _value=1): + # type: (int | str) -> None + super(PhDishwasherType, self).__init__(_value) + + +class PhClothesWasherType(enumerables.CustomEnum): + allowed = [ + "1-DHW CONNECTION", + "2-COLD WATER CONNECTION", + ] + + def __init__(self, _value=1): + # type: (int | str) -> None + super(PhClothesWasherType, self).__init__(_value) + + +class PhClothesDryerType(enumerables.CustomEnum): + allowed = [ + "1-CLOTHES LINE", + "2-DRYING CLOSET (COLD!)", + "3-DRYING CLOSET (COLD!) IN EXTRACT AIR", + "4-CONDENSATION DRYER", + "5-ELECTRIC EXHAUST AIR DRYER", + "6-GAS EXHAUST AIR DRYER", + ] + + def __init__(self, _value=1): + # type: (int | str) -> None + super(PhClothesDryerType, self).__init__(_value) + + +class PhCookingType(enumerables.CustomEnum): + allowed = [ + "1-ELECTRICITY", + "2-NATURAL GAS", + "3-LPG", + ] + + def __init__(self, _value=1): + # type: (int | str) -> None + super(PhCookingType, self).__init__(_value) + diff --git a/honeybee_energy_ph/load/ph_equipment.py b/honeybee_energy_ph/load/ph_equipment.py index ce2195f..bec4f7c 100644 --- a/honeybee_energy_ph/load/ph_equipment.py +++ b/honeybee_energy_ph/load/ph_equipment.py @@ -6,7 +6,7 @@ import sys try: - from typing import Any, Dict, Optional, Union + from typing import Any, Type, Iterator, ValuesView, ItemsView, KeysView except ImportError: pass # IronPython @@ -16,14 +16,12 @@ raise ImportError("Failed to import room: {}".format(e)) try: - from honeybee_energy.properties.room import RoomEnergyProperties + from honeybee_energy.schedule.ruleset import ScheduleRuleset except ImportError as e: raise ImportError("Failed to import RoomEnergyProperties: {}".format(e)) try: from honeybee_energy_ph.load import _base - from honeybee_energy_ph.properties.load.people import PeoplePhProperties - except ImportError as e: raise ImportError("Failed to import honeybee_energy_ph: {}".format(e)) @@ -32,67 +30,29 @@ if TYPE_CHECKING: from honeybee_energy_ph.properties.load.equipment import ElectricEquipmentPhProperties + from honeybee_energy_ph.properties.load.process import ProcessPhProperties except ImportError as e: pass # IronPython try: - from honeybee_ph_utils import enumerables from honeybee_ph_utils.input_tools import input_to_int except ImportError as e: raise ImportError("Failed to import honeybee_ph_utils: {}".format(e)) -# ----------------------------------------------------------------------------- -# - Type Enums - - -class PhDishwasherType(enumerables.CustomEnum): - allowed = [ - "1-DHW CONNECTION", - "2-COLD WATER CONNECTION", - ] - - def __init__(self, _value=1): - # type: (Union[int, str]) -> None - super(PhDishwasherType, self).__init__(_value) - - -class PhClothesWasherType(enumerables.CustomEnum): - allowed = [ - "1-DHW CONNECTION", - "2-COLD WATER CONNECTION", - ] - - def __init__(self, _value=1): - # type: (Union[int, str]) -> None - super(PhClothesWasherType, self).__init__(_value) - - -class PhClothesDryerType(enumerables.CustomEnum): - allowed = [ - "1-CLOTHES LINE", - "2-DRYING CLOSET (COLD!)", - "3-DRYING CLOSET (COLD!) IN EXTRACT AIR", - "4-CONDENSATION DRYER", - "5-ELECTRIC EXHAUST AIR DRYER", - "6-GAS EXHAUST AIR DRYER", - ] - - def __init__(self, _value=1): - # type: (Union[int, str]) -> None - super(PhClothesDryerType, self).__init__(_value) - - -class PhCookingType(enumerables.CustomEnum): - allowed = [ - "1-ELECTRICITY", - "2-NATURAL GAS", - "3-LPG", - ] - - def __init__(self, _value=1): - # type: (Union[int, str]) -> None - super(PhCookingType, self).__init__(_value) +try: + from honeybee_ph_standards.programtypes.default_elec_equip import ph_default_equip +except ImportError as e: + raise ImportError("\nFailed to import honeybee_ph_standards:\n\t{}".format(e)) +try: + from honeybee_energy_ph.load._ph_equip_types import ( + PhDishwasherType, + PhClothesWasherType, + PhClothesDryerType, + PhCookingType, + ) +except ImportError as e: + raise ImportError("Failed to import PhEquipment types: {}".format(e)) # ----------------------------------------------------------------------------- # - Appliance Base @@ -101,8 +61,13 @@ def __init__(self, _value=1): class PhEquipment(_base._Base): """Base for PH Equipment / Appliances with the common attributes.""" - def __init__(self): + _phi_default = None + _phius_default = None + + def __init__(self, _host=None, _defaults={}): + # type: (ProcessPhProperties | None, dict) -> None super(PhEquipment, self).__init__() + self.host = _host self.equipment_type = self.__class__.__name__ self.display_name = "_unnamed_equipment_" self.comment = "" @@ -115,7 +80,7 @@ def __init__(self): self.combined_energy_factor = 0.0 # CEF def apply_default_attr_values(self, _defaults={}): - # type: (Dict[str, Any]) -> None + # type: (dict[str, Any]) -> None """Sets all the object attributes to default values, as specified in a "defaults" dict.""" if not _defaults: @@ -124,8 +89,8 @@ def apply_default_attr_values(self, _defaults={}): for k, v in _defaults.items(): setattr(self, k, v) - def to_dict(self): - # type: () -> Dict[str, Any] + def to_dict(self, _abridged=False): + # type: (bool) -> dict[str, Any] d = {} d["display_name"] = self.display_name @@ -143,9 +108,18 @@ def to_dict(self): d["combined_energy_factor"] = self.combined_energy_factor return d + + @classmethod + def from_dict(cls, _input_dict): + # type: (dict) -> PhEquipment + """Set the object attributes from a dictionary""" + + # -- To be implemented by the equipment, as appropriate. + + raise NotImplementedError(cls) def base_attrs_from_dict(self, _obj, _input_dict): - # type: (PhEquipment, Dict[str, Any]) -> None + # type: (PhEquipment, dict[str, Any]) -> None """Set the base object attributes from a dictionary Arguments: @@ -196,29 +170,59 @@ def merge(self, other, weighting_1=1.0, weighting_2=1.0): return self - def annual_energy_kWh(self, _ref_room=None): - # type: (room.Room | None) -> float + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, *Any) -> float """Returns the annual energy use (kWh) of the equipment.""" # -- To be implemented by the equipment, as appropriate. raise NotImplementedError(self) - def annual_avg_wattage(self, _ref_room=None): - # type: (room.Room | None) -> float + def annual_avg_wattage(self, _schedule=None, *args, **kwargs): + # type: ( ScheduleRuleset | None, *Any, **Any) -> float """Returns the annual average wattage of the equipment.""" - return (self.annual_energy_kWh(_ref_room) * 1000) / 8760 + + if _schedule is not None: + # -- Consider the host schedule.... + sched_factor_sum = sum(_schedule.values()) + else: + sched_factor_sum = 8760 + + annual_energy_Wh = (self.annual_energy_kWh(*args, **kwargs) * 1000) + return annual_energy_Wh / sched_factor_sum def __str__(self): - return "{}(name={}, {})".format( + # type: () -> str + return "{}(display_name={}, {})".format( self.__class__.__name__, self.display_name, ", ".join(["{}={}".format(str(k), str(v)) for k, v, in vars(self).items()]), ) - + def __repr__(self): + # type: () -> str return str(self) + + def ToString(self): + # type: () -> str + return str(self) + + @classmethod + def phius_default(cls): + # type: () -> 'PhEquipment' + """Return the default instance of the object.""" + if not cls._phius_default: + cls._phius_default = cls(_defaults=ph_default_equip[cls.__name__]["PHIUS"]) + return cls._phius_default + @classmethod + def phi_default(cls): + # type: () -> 'PhEquipment' + """Return the default instance of the object.""" + if not cls._phi_default: + cls._phi_default = cls(_defaults=ph_default_equip[cls.__name__]["PHI"]) + return cls._phi_default + # ----------------------------------------------------------------------------- # - Appliances @@ -239,15 +243,15 @@ def water_connection(self): @water_connection.setter def water_connection(self, _input): - # type: (Optional[Union[str, int]]) -> None + # type: (str | int | None) -> None if _input: _input = input_to_int(_input) if not _input: raise ValueError("Invalid input for water_connection: {}".format(_input)) self._water_connection = PhDishwasherType(_input) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhDishwasher, self).to_dict()) d["capacity_type"] = self.capacity_type @@ -265,7 +269,8 @@ def from_dict(cls, _input_dict): new_obj._water_connection = PhDishwasherType.from_dict(_input_dict["_water_connection"]) return new_obj - def annual_energy_kWh(self, _ref_room=None): + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float return self.energy_demand @@ -285,15 +290,15 @@ def water_connection(self): @water_connection.setter def water_connection(self, _input): - # type: (Optional[Union[str, int]]) -> None + # type: (str | int | None) -> None if _input: _input = input_to_int(_input) if not _input: raise ValueError("Invalid input for water_connection: {}".format(_input)) self._water_connection = PhClothesWasherType(_input) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhClothesWasher, self).to_dict()) d["capacity"] = self.capacity @@ -313,7 +318,8 @@ def from_dict(cls, _input_dict): new_obj.utilization_factor = _input_dict["utilization_factor"] return new_obj - def annual_energy_kWh(self, _ref_room=None): + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float return self.energy_demand @@ -334,15 +340,15 @@ def dryer_type(self): @dryer_type.setter def dryer_type(self, _input): - # type: (Optional[Union[int, str]]) -> None + # type: (str | int | None) -> None if _input: _input = input_to_int(_input) if not _input: raise ValueError("Invalid input for dryer_type: {}".format(_input)) self._dryer_type = PhClothesDryerType(_input) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhClothesDryer, self).to_dict()) d["_dryer_type"] = self._dryer_type.to_dict() @@ -364,11 +370,20 @@ def from_dict(cls, _input_dict): new_obj.field_utilization_factor = _input_dict["field_utilization_factor"] return new_obj - def annual_energy_kWh(self, _ref_room=None): - # TODO: Figure out how they calculate dryer energy? ANSI/Resnet? - - return 0.0 - + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float + """Returns the annual energy use (kWh) of the equipment.""" + # -- Appendix N - Normative | Phius 2024 Certification Guidebook v24.1.1 + # -- Table 11.6.4.0 prescriptive Path Appliance, Lighting and DHW REference Efficiencies + + _num_occupants = kwargs.get("_num_occupants", None) + if _num_occupants is None: + raise ValueError("'_num_occupants' input is required for the annual_energy_kWh method. Got only: {}".format(kwargs)) + try: + return _num_occupants * ( 283 / 4.5 ) / self.combined_energy_factor * 8.45 + except ZeroDivisionError: + return 0 + class PhRefrigerator(PhEquipment): def __init__(self, _defaults={}): @@ -376,8 +391,8 @@ def __init__(self, _defaults={}): self.display_name = "Kitchen refrigerator" self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhRefrigerator, self).to_dict()) @@ -391,7 +406,8 @@ def from_dict(cls, _input_dict): return new_obj - def annual_energy_kWh(self, _ref_room=None): + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float return self.energy_demand * 365 @@ -401,8 +417,8 @@ def __init__(self, _defaults={}): self.display_name = "Kitchen freezer" self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhFreezer, self).to_dict()) @@ -416,7 +432,8 @@ def from_dict(cls, _input_dict): return new_obj - def annual_energy_kWh(self, _ref_room=None): + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float return self.energy_demand * 365 @@ -426,8 +443,8 @@ def __init__(self, _defaults={}): self.display_name = "Kitchen fridge/freeze combo" self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhFridgeFreezer, self).to_dict()) @@ -441,7 +458,8 @@ def from_dict(cls, _input_dict): return new_obj - def annual_energy_kWh(self, _ref_room=None): + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float return self.energy_demand * 365 @@ -458,15 +476,15 @@ def cooktop_type(self): @cooktop_type.setter def cooktop_type(self, _input): - # type: (Optional[Union[str, int]]) -> None + # type: (str | int | None) -> None if _input: _input = input_to_int(_input) if not _input: raise ValueError("Invalid input for cooktop_type: {}".format(_input)) self._cooktop_type = PhCookingType(_input) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhCooktop, self).to_dict()) d["_cooktop_type"] = self._cooktop_type.to_dict() @@ -480,19 +498,19 @@ def from_dict(cls, _input_dict): new_obj._cooktop_type = PhCookingType.from_dict(_input_dict["_cooktop_type"]) return new_obj - def annual_energy_kWh(self, _ref_room=None): - # type: (room.Room | None) -> float + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float """Returns the annual energy use (kWh) of the equipment. Assuming a number of meals as per Phius Guidebook V3.02, pg 73 footnote #31 """ - if not _ref_room: - return 0 + + _num_occupants = kwargs.get("_num_occupants", None) + if _num_occupants is None: + raise ValueError("'_num_occupants' input is required for the annual_energy_kWh method.") annual_meals_per_occupant = 500 - hbe_room_prop = getattr(_ref_room.properties, "energy") # type: RoomEnergyProperties - hbph_people_prop = getattr(hbe_room_prop.people.properties, "ph") # type: PeoplePhProperties - num_meals = hbph_people_prop.number_people * annual_meals_per_occupant + num_meals = _num_occupants * annual_meals_per_occupant return self.energy_demand * num_meals @@ -502,8 +520,8 @@ def __init__(self, _defaults={}): self.display_name = "PHIUS+ MELS" self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhPhiusMEL, self).to_dict()) @@ -517,7 +535,39 @@ def from_dict(cls, _input_dict): return new_obj - # TODO: annual_avg_wattage + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float + """Return the Phius Misc. Electrical Loads (MEL) for a single dwelling [kWh]. + + ### Resnet 2014 + - https://codes.iccsafe.org/content/RESNET3012014P1/4-home-energy-rating-calculation-procedures- + - Section 4.2.2.5(1): Energy Rating Reference Home + - kWh = 413 + 0.91 * CFA + 69 * Nbr + + ### Phius Certification Guidebook v24.1.1 | Appendix N | N-7 + - https://www.phius.org/phius-certification-guidebook + - "The basic protocol for lighting and miscellaneous electric loads is that they are calculated at + 80% of RESNET (2013) levels for the 'Rated Home'." + - kWh = (413 + 69 * Nbr + 0.91 * CFA) * 0.8 + """ + _num_bedrooms = kwargs.get("_num_bedrooms", None) + if _num_bedrooms is None: + raise ValueError("'_num_bedrooms' input is required for the annual_energy_kWh method.") + + _floor_area_ft2 = kwargs.get("_floor_area_ft2", None) + if _floor_area_ft2 is None: + raise ValueError("'_floor_area_ft2' input is required for the annual_energy_kWh method.") + + DWELLING_TV_KWH_YR = 413 + BEDROOM_TV_KWH_YR = 69 + MELS_KWH_YR_FT2 = 0.91 + PHIUS_RESNET_FRACTION = 0.8 + + a = DWELLING_TV_KWH_YR + b = BEDROOM_TV_KWH_YR * _num_bedrooms + c = MELS_KWH_YR_FT2 * _floor_area_ft2 + + return (a + b + c) * PHIUS_RESNET_FRACTION class PhPhiusLightingInterior(PhEquipment): @@ -527,8 +577,8 @@ def __init__(self, _defaults={}): self.frac_high_efficiency = 1 # CEF self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhPhiusLightingInterior, self).to_dict()) d["frac_high_efficiency"] = self.frac_high_efficiency @@ -542,20 +592,52 @@ def from_dict(cls, _input_dict): new_obj.frac_high_efficiency = _input_dict["frac_high_efficiency"] return new_obj - # TODO: annual_avg_wattage + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float + """Returns the Phius Interior Lighting energy consumption for a single dwelling [kWh]. + + ### Resnet 2014 + - https://codes.iccsafe.org/content/RESNET3012014P1/4-home-energy-rating-calculation-procedures- + - Section 4.2.2.5.2.2: Interior Lighting + - kWh/yr = 0.8 * [(4 - 3 * q_FFIL) / 3.7] * (455 + 0.8 * CFA) + 0.2 * (455 + 0.8 * CFA) + + ### Phius Certification Guidebook v24.1.1 | Appendix N | N-7 + - https://www.phius.org/phius-certification-guidebook + - "The basic protocol for lighting and miscellaneous electric loads is that they are calculated at + 80% of RESNET (2013) levels for the 'Rated Home'. ... The RESNET lighting formulas have been expressed more + compactly here but are algebraically equivalent to the published versions." + - kWh/yr = (0.2 + 0.8 * (4 - 3 * q_FFIL) / 3.7) * (455 + 0.8 * iCFA) * 0.8 + """ + + _num_bedrooms = kwargs.get("_num_bedrooms", None) + if _num_bedrooms is None: + raise ValueError("'_num_bedrooms' input is required for the annual_energy_kWh method.") + + _floor_area_ft2 = kwargs.get("_floor_area_ft2", None) + if _floor_area_ft2 is None: + raise ValueError("'_floor_area_ft2' input is required for the annual_energy_kWh method.") + + INT_LIGHTING_W_PER_DWELLING = 455 + INT_LIGHTING_W_FT2 = 0.8 + PHIUS_RESNET_FRACTION = 0.8 + + a = 0.2 + 0.8 * (4 - 3 * self.frac_high_efficiency) / 3.7 + b = INT_LIGHTING_W_PER_DWELLING + (INT_LIGHTING_W_FT2 * _floor_area_ft2) + + return a * b * PHIUS_RESNET_FRACTION class PhPhiusLightingExterior(PhEquipment): def __init__(self, _defaults={}): - # type: (Dict[str, Any]) -> None + # type: (dict[str, Any]) -> None super(PhPhiusLightingExterior, self).__init__() self.display_name = "PHIUS+ Exterior Lighting" self.frac_high_efficiency = 1 # CEF self.in_conditioned_space = False self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> Dict[str, Any] + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhPhiusLightingExterior, self).to_dict()) d["frac_high_efficiency"] = self.frac_high_efficiency @@ -563,13 +645,46 @@ def to_dict(self): @classmethod def from_dict(cls, _input_dict): - # type: (Dict[str, Any]) -> PhPhiusLightingExterior + # type: (dict[str, Any]) -> PhPhiusLightingExterior new_obj = cls() super(PhPhiusLightingExterior, new_obj).base_attrs_from_dict(new_obj, _input_dict) new_obj.frac_high_efficiency = _input_dict["frac_high_efficiency"] return new_obj - # TODO: annual_avg_wattage + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float + """Returns the Phius Exterior Lighting energy consumption for a single dwelling [kWh]. + + ### Resnet 2014 + - https://codes.iccsafe.org/content/RESNET3012014P1/4-home-energy-rating-calculation-procedures- + - Section 4.2.2.5.2.3: Exterior Lighting + - kWh = (100+0.05*FCA)*(1-FF_El)+0.25*(100+0.05*CFA)*FF_EL + + ### Phius Certification Guidebook v24.1.1 | Appendix N | N-7 + - https://www.phius.org/phius-certification-guidebook + - "The basic protocol for lighting and miscellaneous electric loads is that they are calculated at + 80% of RESNET (2013) levels for the 'Rated Home'. ... The RESNET lighting formulas have been expressed more + compactly here but are algebraically equivalent to the published versions." + - kWh/yr = (1 - 0.75 * q_FFIL) * (100 + 0.05 * iCFA) * 0.8 + """ + + _num_bedrooms = kwargs.get("_num_bedrooms", None) + if _num_bedrooms is None: + raise ValueError("'_num_bedrooms' input is required for the annual_energy_kWh method.") + + _floor_area_ft2 = kwargs.get("_floor_area_ft2", None) + if _floor_area_ft2 is None: + raise ValueError("'_floor_area_ft2' input is required for the annual_energy_kWh method.") + + EXT_LIGHTING_KWH_YR_PER_DWELLING = 100 + EXT_LIGHTING_KWH_YR_FT2 = 0.05 + PHIUS_RESNET_FRACTION = 0.8 + + a = EXT_LIGHTING_KWH_YR_PER_DWELLING + b = EXT_LIGHTING_KWH_YR_FT2 * _floor_area_ft2 + e = 1 - 0.75 * self.frac_high_efficiency + + return e * (a + b) * PHIUS_RESNET_FRACTION class PhPhiusLightingGarage(PhEquipment): @@ -580,8 +695,8 @@ def __init__(self, _defaults={}): self.in_conditioned_space = False self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> Dict[str, Any] + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhPhiusLightingGarage, self).to_dict()) d["frac_high_efficiency"] = self.frac_high_efficiency @@ -589,13 +704,34 @@ def to_dict(self): @classmethod def from_dict(cls, _input_dict): - # type: (Dict[str, Any]) -> PhPhiusLightingGarage + # type: (dict[str, Any]) -> PhPhiusLightingGarage new_obj = cls() super(PhPhiusLightingGarage, new_obj).base_attrs_from_dict(new_obj, _input_dict) new_obj.frac_high_efficiency = _input_dict["frac_high_efficiency"] return new_obj + + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float + """Returns the Phius Garage Lighting energy consumption for a single dwelling [kWh]. + + ### Resnet 2014 + - https://codes.iccsafe.org/content/RESNET3012014P1/4-home-energy-rating-calculation-procedures- + - Section 4.2.2.5.1.3: Garage Lighting + - kWh = 100/dwelling + + ### Phius Certification Guidebook v24.1.1 | Appendix N | N-7 + - https://www.phius.org/phius-certification-guidebook + - "The basic protocol for lighting and miscellaneous electric loads is that they are calculated at + 80% of RESNET (2013) levels for the 'Rated Home'. ... The RESNET lighting formulas have been expressed more + compactly here but are algebraically equivalent to the published versions." + - kWh/yr = 100 * (1 - 0.75 * FFGL) * 0.8 + """ + + GARAGE_LIGHTING_KWH_YR_PER_DWELLING = 100 + PHIUS_RESNET_FRACTION = 0.8 + e = 1 - 0.75 * self.frac_high_efficiency - # TODO: annual_avg_wattage + return GARAGE_LIGHTING_KWH_YR_PER_DWELLING * e * PHIUS_RESNET_FRACTION class PhCustomAnnualElectric(PhEquipment): @@ -604,8 +740,8 @@ def __init__(self, _defaults={}): self.display_name = "User defined" self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhCustomAnnualElectric, self).to_dict()) d["energy_demand"] = self.energy_demand @@ -619,7 +755,8 @@ def from_dict(cls, _input_dict): new_obj.energy_demand = _input_dict["energy_demand"] return new_obj - def annual_energy_kWh(self, _ref_room=None): + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float return self.energy_demand @@ -629,8 +766,8 @@ def __init__(self, _defaults={}): self.display_name = "User defined - lighting" self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhCustomAnnualLighting, self).to_dict()) d["energy_demand"] = self.energy_demand @@ -644,7 +781,8 @@ def from_dict(cls, _input_dict): new_obj.energy_demand = _input_dict["energy_demand"] return new_obj - def annual_energy_kWh(self, _ref_room=None): + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float return self.energy_demand @@ -654,8 +792,8 @@ def __init__(self, _defaults={}): self.display_name = "User defined - Misc electric loads" self.apply_default_attr_values(_defaults) - def to_dict(self): - # type: () -> dict + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhCustomAnnualMEL, self).to_dict()) d["energy_demand"] = self.energy_demand @@ -669,7 +807,8 @@ def from_dict(cls, _input_dict): new_obj.energy_demand = _input_dict["energy_demand"] return new_obj - def annual_energy_kWh(self, _ref_room=None): + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float return self.energy_demand @@ -695,19 +834,23 @@ def set_energy_demand(self, _num_dwellings): else: self.energy_demand = 4120.0 - def to_dict(self): - # type: () -> Dict[str, Any] + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhElevatorHydraulic, self).to_dict()) return d @classmethod def from_dict(cls, _input_dict): - # type: (Dict[str, Any]) -> PhElevatorHydraulic + # type: (dict[str, Any]) -> PhElevatorHydraulic new_obj = cls() super(PhElevatorHydraulic, new_obj).base_attrs_from_dict(new_obj, _input_dict) return new_obj - + + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float + """Returns the annual energy use (kWh) of the equipment.""" + return self.energy_demand class PhElevatorGearedTraction(PhEquipment): def __init__(self, _num_dwellings=1): @@ -728,19 +871,23 @@ def set_energy_demand(self, _num_dwellings): else: self.energy_demand = 4550.0 - def to_dict(self): - # type: () -> Dict[str, Any] + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhElevatorGearedTraction, self).to_dict()) return d @classmethod def from_dict(cls, _input_dict): - # type: (Dict[str, Any]) -> PhElevatorGearedTraction + # type: (dict[str, Any]) -> PhElevatorGearedTraction new_obj = cls() super(PhElevatorGearedTraction, new_obj).base_attrs_from_dict(new_obj, _input_dict) return new_obj + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float + """Returns the annual energy use (kWh) of the equipment.""" + return self.energy_demand class PhElevatorGearlessTraction(PhEquipment): def __init__(self, _num_dwellings=1): @@ -761,30 +908,37 @@ def set_energy_demand(self, _num_dwellings): else: self.energy_demand = 7570.0 - def to_dict(self): - # type: () -> Dict[str, Any] + def to_dict(self, _abridged=False): + # type: (bool) -> dict d = {} d.update(super(PhElevatorGearlessTraction, self).to_dict()) return d @classmethod def from_dict(cls, _input_dict): - # type: (Dict[str, Any]) -> PhElevatorGearlessTraction + # type: (dict[str, Any]) -> PhElevatorGearlessTraction new_obj = cls() super(PhElevatorGearlessTraction, new_obj).base_attrs_from_dict(new_obj, _input_dict) return new_obj + def annual_energy_kWh(self, *args, **kwargs): + # type: (*Any, **Any) -> float + """Returns the annual energy use (kWh) of the equipment.""" + return self.energy_demand + # ----------------------------------------------------------------------------- # Collections +# TODO: Deprecate these classes in favor of new the "Process Load" method. +# See: honeybee_energy_ph.load.process.py class PhEquipmentBuilder(object): """Constructor class for PH Equipment objects""" @classmethod - def from_dict(cls, _input_dict): - # type: (dict) -> PhEquipment + def from_dict(cls, _input_dict, _host=None): + # type: (dict, ProcessPhProperties | None) -> PhEquipment """Find the right appliance constructor class from the module based on the 'type' name.""" equipment_type = _input_dict["equipment_type"] @@ -795,18 +949,21 @@ def from_dict(cls, _input_dict): ) raise Exception(msg) - equipment_class = getattr(sys.modules[__name__], equipment_type) + equipment_class = getattr(sys.modules[__name__], equipment_type) # type: Type[PhEquipment] new_equipment = equipment_class.from_dict(_input_dict) - + return new_equipment def __str__(self): + # type: () -> str return "{}()".format(self.__class__.__name__) def __repr__(self): + # type: () -> str return str(self) def ToString(self): + # type: () -> str return str(self) @@ -816,22 +973,26 @@ class PhEquipmentCollection(object): This is stored on the Honeybee-Room's properties.energy.electric_equipment.properties.ph """ - def __init__(self, _host): - self._equipment_set = {} + def __init__(self, _host=None): + # type: (ElectricEquipmentPhProperties | None) -> None + self._equipment_set = {} # type: dict[str, PhEquipment] self._host = _host @property def host(self): - # type: () -> ElectricEquipmentPhProperties + # type: () -> ElectricEquipmentPhProperties | None return self._host def items(self): + # type: () -> ItemsView[str, PhEquipment] return self._equipment_set.items() def keys(self): + # type: () -> KeysView[str] return self._equipment_set.keys() def values(self): + # type: () -> ValuesView[PhEquipment] return self._equipment_set.values() def duplicate(self, new_host=None): @@ -863,6 +1024,7 @@ def add_equipment(self, _new_equipment, _key=None): return None def remove_all_equipment(self): + # type: () -> None """Reset the Collection to an empty set.""" self._equipment_set = {} @@ -904,13 +1066,16 @@ def from_dict(cls, _input_dict, _host): return new_obj def __iter__(self): + # type: () -> Iterator[tuple[str, PhEquipment]] for _ in self._equipment_set.items(): yield _ def __setitem__(self, key, attr): + # type: (str, PhEquipment) -> None self._equipment_set[key] = attr def __getitem__(self, key): + # type: (str) -> PhEquipment return self._equipment_set[key] def __copy__(self, new_host=None): @@ -924,10 +1089,13 @@ def __copy__(self, new_host=None): return new_obj def __str__(self): + # type: () -> str return "{}({} pieces of equipment)".format(self.__class__.__name__, len(self._equipment_set.keys())) def __repr__(self): + # type: () -> str return str(self) def ToString(self): + # type: () -> str return str(self) diff --git a/honeybee_energy_ph/properties/load/equipment.py b/honeybee_energy_ph/properties/load/equipment.py index 5865ba7..3bd244b 100644 --- a/honeybee_energy_ph/properties/load/equipment.py +++ b/honeybee_energy_ph/properties/load/equipment.py @@ -23,7 +23,9 @@ def __init__(self, _expected_types, _input_type): class ElectricEquipmentPhProperties(object): def __init__(self, _host): self._host = _host - self.equipment_collection = ph_equipment.PhEquipmentCollection(self) + + # TODO: Deprecate in favor of new (Jan 2025) 'Process' Load method + self.equipment_collection = ph_equipment.PhEquipmentCollection(self) @property def host(self): @@ -61,7 +63,7 @@ def from_dict(cls, _input_dict, _host): return new_prop def apply_properties_from_dict(self, abridged_data): - return + return None def __copy__(self, new_host=None): # type: (Any) -> ElectricEquipmentPhProperties diff --git a/honeybee_energy_ph/properties/load/process.py b/honeybee_energy_ph/properties/load/process.py new file mode 100644 index 0000000..feed087 --- /dev/null +++ b/honeybee_energy_ph/properties/load/process.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# -*- Python Version: 2.7 -*- + +"""Process Equipment PH-Properties""" + +try: + from typing import Any +except: + pass # IronPython + + +try: + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from honeybee_energy.properties.extension import ProcessProperties +except ImportError as e: + pass # IronPython + + +try: + from honeybee_energy_ph.load.ph_equipment import PhEquipment, PhEquipmentBuilder +except ImportError as e: + raise ImportError('Failed to import honeybee_energy_ph', e) + + +class ProcessPhProperties_FromDictError(Exception): + def __init__(self, _expected_types, _input_type): + self.msg = 'Error: Expected type of "{}". Got: {}'.format(_expected_types, _input_type) + super(ProcessPhProperties_FromDictError, self).__init__(self.msg) + + +class ProcessPhProperties(object): + def __init__(self, _host): + # type: (ProcessProperties) -> None + self._host = _host + self._ph_equipment = None + + @property + def ph_equipment(self): + # type: () -> PhEquipment | None + return self._ph_equipment + + @ph_equipment.setter + def ph_equipment(self, _equipment): + # type: (PhEquipment) -> None + if not isinstance(_equipment, PhEquipment): + raise ValueError("Input must be of type PhEquipment") + self._ph_equipment = _equipment + + @property + def host(self): + # type: () -> ProcessProperties + return self._host + + def to_dict(self, abridged=False): + # type: (bool) -> dict + d = {} + + if abridged: + d["type"] = "ProcessPhPropertiesAbridged" + else: + d["type"] = "ProcessPhProperties" + + if self._ph_equipment: + d["equipment"] = self._ph_equipment.to_dict(_abridged=abridged) + + return {"ph": d} + + @classmethod + def from_dict(cls, _input_dict, _host): + # type: (dict, Any) -> ProcessPhProperties + valid_types = ( + "ProcessPhProperties", + "ProcessPhPropertiesAbridged", + ) + if _input_dict["type"] not in valid_types: + raise ProcessPhProperties_FromDictError(valid_types, _input_dict["type"]) + + new_prop = cls(_host) + + if "equipment" in _input_dict: + new_prop.ph_equipment = PhEquipmentBuilder.from_dict(_input_dict["equipment"], _host=new_prop) + + return new_prop + + def apply_properties_from_dict(self, abridged_data): + # type: (dict) -> None + return None + + def __copy__(self, new_host=None): + # type: (ProcessProperties | None) -> ProcessPhProperties + host = new_host or self._host + new_obj = self.__class__(host) + if self._ph_equipment: + new_obj.ph_equipment = PhEquipmentBuilder.from_dict(self._ph_equipment.to_dict(), _host=new_obj) + return new_obj + + def duplicate(self, new_host=None): + # type: (Any) -> ProcessPhProperties + return self.__copy__(new_host) + + def __str__(self): + return "{}()".format(self.__class__.__name__) + + def __repr__(self): + return str(self) + + def ToString(self): + return str(self) diff --git a/honeybee_ph_standards/programtypes/default_elec_equip.py b/honeybee_ph_standards/programtypes/default_elec_equip.py index c794b53..62fc345 100644 --- a/honeybee_ph_standards/programtypes/default_elec_equip.py +++ b/honeybee_ph_standards/programtypes/default_elec_equip.py @@ -3,7 +3,7 @@ """Default data for Residential PH Electrical Appliances (PHI and Phius).""" -from honeybee_energy_ph.load.ph_equipment import ( +from honeybee_energy_ph.load._ph_equip_types import ( PhClothesDryerType, PhClothesWasherType, PhCookingType, @@ -130,8 +130,8 @@ "quantity": 1, "in_conditioned_space": True, "reference_energy_norm": 1, # Day - "energy_demand": 0, - "energy_demand_per_use": 0.88, + "energy_demand": 1.0795, # 394 kWh/year + "energy_demand_per_use": 0, "combined_energy_factor": 0, }, "PHIUS": { @@ -140,8 +140,8 @@ "quantity": 1, "in_conditioned_space": True, "reference_energy_norm": 1, # Day - "energy_demand": 0, - "energy_demand_per_use": 0.88, + "energy_demand": 1.0795, # 394 kWh/year + "energy_demand_per_use": 0, "combined_energy_factor": 0, }, }, diff --git a/honeybee_ph_standards/schedules/__init__.py b/honeybee_ph_standards/schedules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/honeybee_ph_standards/schedules/_load_schedules.py b/honeybee_ph_standards/schedules/_load_schedules.py new file mode 100644 index 0000000..ee093d2 --- /dev/null +++ b/honeybee_ph_standards/schedules/_load_schedules.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# -*- Python Version: 2.7 -*- + +"""Utility function to load HB-E-Schedule Objects from a JSON file.""" + +import json +import os + +try: + from honeybee_energy.schedule.ruleset import ScheduleRuleset +except ImportError as e: + raise ImportError("\nFailed to import honeybee_energy:\n\t{}".format(e)) + +def is_schedule(_json_object): + # type: (dict) -> bool + """Check if a JSON object is a valid 'ScheduleRuleset' dict.""" + if "type" not in _json_object: + return False + if not _json_object["type"] == "ScheduleRuleset": + return False + return True + + +def load_schedules_from_json_file(_schedules_filepath): + # type: (str) -> dict[str, ScheduleRuleset] + """Load a set of HBE-ScheduleRuleset object from a JSON file.""" + if not os.path.exists(_schedules_filepath): + raise ValueError("File not found: {}".format(_schedules_filepath)) + + with open(_schedules_filepath, "r") as json_file: + all_schedules = (ScheduleRuleset.from_dict(d) for d in json.load(json_file) if is_schedule(d)) + return {_.identifier: _ for _ in all_schedules} diff --git a/honeybee_ph_standards/schedules/hbph_sfh_appliances.json b/honeybee_ph_standards/schedules/hbph_sfh_appliances.json new file mode 100644 index 0000000..b3b8295 --- /dev/null +++ b/honeybee_ph_standards/schedules/hbph_sfh_appliances.json @@ -0,0 +1,572 @@ +[ + { + "display_name": "hbph_sfh_Dishwasher", + "identifier": "hbph_sfh_Dishwasher", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_Dishwasher_Day Schedule", + "day_schedules": [ + { + "values": [ + 0.014999999999999999, + 0.0070000000000000001, + 0.0050000000000000001, + 0.0030000000000000001, + 0.01, + 0.02, + 0.031, + 0.058000000000000003, + 0.065000000000000002, + 0.056000000000000001, + 0.048000000000000001, + 0.041000000000000002, + 0.045999999999999999, + 0.035999999999999997, + 0.037999999999999999, + 0.049000000000000002, + 0.086999999999999994, + 0.111, + 0.089999999999999997, + 0.067000000000000004, + 0.043999999999999997, + 0.031 + ], + "identifier": "hbph_sfh_Dishwasher_Day Schedule", + "type": "ScheduleDay", + "times": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 2, + 0 + ], + [ + 3, + 0 + ], + [ + 5, + 0 + ], + [ + 6, + 0 + ], + [ + 7, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 10, + 0 + ], + [ + 11, + 0 + ], + [ + 12, + 0 + ], + [ + 13, + 0 + ], + [ + 14, + 0 + ], + [ + 15, + 0 + ], + [ + 17, + 0 + ], + [ + 18, + 0 + ], + [ + 19, + 0 + ], + [ + 20, + 0 + ], + [ + 21, + 0 + ], + [ + 22, + 0 + ], + [ + 23, + 0 + ] + ], + "interpolate": false + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "type": "ScheduleTypeLimit", + "identifier": "Fractional", + "unit_type": "Dimensionless", + "lower_limit": 0.0 + }, + "properties": {} + }, + { + "display_name": "hbph_sfh_Clotheswasher", + "identifier": "hbph_sfh_Clotheswasher", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_Clotheswasher_Day Schedule", + "day_schedules": [ + { + "values": [ + 0.0089999999999999993, + 0.0070000000000000001, + 0.0040000000000000001, + 0.0070000000000000001, + 0.010999999999999999, + 0.021999999999999999, + 0.049000000000000002, + 0.072999999999999995, + 0.085999999999999993, + 0.084000000000000005, + 0.074999999999999997, + 0.067000000000000004, + 0.059999999999999998, + 0.049000000000000002, + 0.051999999999999998, + 0.050000000000000003, + 0.049000000000000002, + 0.047, + 0.032000000000000001, + 0.017000000000000001 + ], + "identifier": "hbph_sfh_Clotheswasher_Day Schedule", + "type": "ScheduleDay", + "times": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 2, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 0 + ], + [ + 6, + 0 + ], + [ + 7, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 10, + 0 + ], + [ + 11, + 0 + ], + [ + 12, + 0 + ], + [ + 13, + 0 + ], + [ + 14, + 0 + ], + [ + 15, + 0 + ], + [ + 16, + 0 + ], + [ + 17, + 0 + ], + [ + 21, + 0 + ], + [ + 22, + 0 + ], + [ + 23, + 0 + ] + ], + "interpolate": false + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "type": "ScheduleTypeLimit", + "identifier": "Fractional", + "unit_type": "Dimensionless", + "lower_limit": 0.0 + }, + "properties": {} + }, + { + "display_name": "hbph_sfh_Clothesdryer", + "identifier": "hbph_sfh_Clothesdryer", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_Clothesdryer_Day Schedule", + "day_schedules": [ + { + "values": [ + 0.01, + 0.0060000000000000001, + 0.0040000000000000001, + 0.002, + 0.0040000000000000001, + 0.0060000000000000001, + 0.016, + 0.032000000000000001, + 0.048000000000000001, + 0.068000000000000005, + 0.078, + 0.081000000000000003, + 0.073999999999999996, + 0.067000000000000004, + 0.057000000000000002, + 0.060999999999999999, + 0.055, + 0.053999999999999999, + 0.050999999999999997, + 0.051999999999999998, + 0.053999999999999999, + 0.043999999999999997, + 0.024 + ], + "identifier": "hbph_sfh_Clothesdryer_Day Schedule", + "type": "ScheduleDay", + "times": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 2, + 0 + ], + [ + 3, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 0 + ], + [ + 6, + 0 + ], + [ + 7, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 10, + 0 + ], + [ + 11, + 0 + ], + [ + 12, + 0 + ], + [ + 13, + 0 + ], + [ + 14, + 0 + ], + [ + 15, + 0 + ], + [ + 16, + 0 + ], + [ + 17, + 0 + ], + [ + 18, + 0 + ], + [ + 20, + 0 + ], + [ + 21, + 0 + ], + [ + 22, + 0 + ], + [ + 23, + 0 + ] + ], + "interpolate": false + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "type": "ScheduleTypeLimit", + "identifier": "Fractional", + "unit_type": "Dimensionless", + "lower_limit": 0.0 + }, + "properties": {} + }, + { + "display_name": "hbph_sfh_Cooktop", + "identifier": "hbph_sfh_Cooktop", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_Cooktop_Day Schedule", + "day_schedules": [ + { + "values": [ + 0.0070000000000000001, + 0.0040000000000000001, + 0.0070000000000000001, + 0.010999999999999999, + 0.025000000000000001, + 0.042000000000000003, + 0.045999999999999999, + 0.048000000000000001, + 0.042000000000000003, + 0.050000000000000003, + 0.057000000000000002, + 0.045999999999999999, + 0.057000000000000002, + 0.043999999999999997, + 0.091999999999999998, + 0.14999999999999999, + 0.11700000000000001, + 0.059999999999999998, + 0.035000000000000003, + 0.025000000000000001, + 0.016, + 0.010999999999999999 + ], + "identifier": "hbph_sfh_Cooktop_Day Schedule", + "type": "ScheduleDay", + "times": [ + [ + 0, + 0 + ], + [ + 2, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 0 + ], + [ + 6, + 0 + ], + [ + 7, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 10, + 0 + ], + [ + 11, + 0 + ], + [ + 12, + 0 + ], + [ + 13, + 0 + ], + [ + 14, + 0 + ], + [ + 15, + 0 + ], + [ + 16, + 0 + ], + [ + 17, + 0 + ], + [ + 18, + 0 + ], + [ + 19, + 0 + ], + [ + 20, + 0 + ], + [ + 21, + 0 + ], + [ + 22, + 0 + ], + [ + 23, + 0 + ] + ], + "interpolate": false + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "type": "ScheduleTypeLimit", + "identifier": "Fractional", + "unit_type": "Dimensionless", + "lower_limit": 0.0 + }, + "properties": {} + }, + { + "display_name": "hbph_sfh_Refrigerator", + "identifier": "hbph_sfh_Refrigerator", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_Refrigerator_Day Schedule", + "day_schedules": [ + { + "values": [ + 1.0 + ], + "identifier": "hbph_sfh_Refrigerator_Day Schedule", + "type": "ScheduleDay", + "times": [ + [ + 0, + 0 + ] + ], + "interpolate": false + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "type": "ScheduleTypeLimit", + "identifier": "Fractional", + "unit_type": "Dimensionless", + "lower_limit": 0.0 + }, + "properties": {} + } +] \ No newline at end of file diff --git a/honeybee_ph_standards/schedules/hbph_sfh_electric_equipment.json b/honeybee_ph_standards/schedules/hbph_sfh_electric_equipment.json new file mode 100644 index 0000000..59a1e27 --- /dev/null +++ b/honeybee_ph_standards/schedules/hbph_sfh_electric_equipment.json @@ -0,0 +1,112 @@ +[ + { + "identifier": "hbph_sfh_MEL", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_MEL_Day Schedule", + "day_schedules": [ + { + "values": [ + 0.0080000000000000002, + 0.024, + 0.050000000000000003, + 0.056000000000000001, + 0.050000000000000003, + 0.021999999999999999, + 0.014999999999999999, + 0.025999999999999999, + 0.014999999999999999, + 0.056000000000000001, + 0.078, + 0.105, + 0.126, + 0.128, + 0.087999999999999995, + 0.049000000000000002, + 0.02 + ], + "interpolate": false, + "identifier": "hbph_sfh_MEL_Day Schedule", + "times": [ + [ + 0, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 0 + ], + [ + 6, + 0 + ], + [ + 7, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 14, + 0 + ], + [ + 15, + 0 + ], + [ + 16, + 0 + ], + [ + 17, + 0 + ], + [ + 18, + 0 + ], + [ + 19, + 0 + ], + [ + 20, + 0 + ], + [ + 21, + 0 + ], + [ + 22, + 0 + ], + [ + 23, + 0 + ] + ], + "type": "ScheduleDay" + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "lower_limit": 0.0, + "unit_type": "Dimensionless", + "identifier": "Fractional", + "type": "ScheduleTypeLimit" + }, + "properties": {} + } +] \ No newline at end of file diff --git a/honeybee_ph_standards/schedules/hbph_sfh_hot_water.json b/honeybee_ph_standards/schedules/hbph_sfh_hot_water.json new file mode 100644 index 0000000..8c6d14d --- /dev/null +++ b/honeybee_ph_standards/schedules/hbph_sfh_hot_water.json @@ -0,0 +1,137 @@ +[ + { + "default_day_schedule": "hbph_sfh_Combined_HotWater_Day Schedule", + "identifier": "hbph_sfh_Combined_HotWater", + "type": "ScheduleRuleset", + "day_schedules": [ + { + "values": [ + 0.0060000000000000001, + 0.0030000000000000001, + 0.001, + 0.0030000000000000001, + 0.021999999999999999, + 0.074999999999999997, + 0.079000000000000001, + 0.075999999999999998, + 0.067000000000000004, + 0.060999999999999999, + 0.048000000000000001, + 0.042000000000000003, + 0.036999999999999998, + 0.033000000000000002, + 0.043999999999999997, + 0.058000000000000003, + 0.069000000000000006, + 0.065000000000000002, + 0.058999999999999997, + 0.048000000000000001, + 0.042000000000000003, + 0.023 + ], + "interpolate": false, + "identifier": "hbph_sfh_Combined_HotWater_Day Schedule", + "times": [ + [ + 0, + 0 + ], + [ + 1, + 0 + ], + [ + 2, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 0 + ], + [ + 6, + 0 + ], + [ + 7, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 10, + 0 + ], + [ + 11, + 0 + ], + [ + 12, + 0 + ], + [ + 13, + 0 + ], + [ + 15, + 0 + ], + [ + 16, + 0 + ], + [ + 17, + 0 + ], + [ + 18, + 0 + ], + [ + 19, + 0 + ], + [ + 20, + 0 + ], + [ + 21, + 0 + ], + [ + 22, + 0 + ], + [ + 23, + 0 + ] + ], + "type": "ScheduleDay" + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "lower_limit": 0.0, + "unit_type": "Dimensionless", + "identifier": "Fractional", + "type": "ScheduleTypeLimit" + }, + "properties": {} + } +] \ No newline at end of file diff --git a/honeybee_ph_standards/schedules/hbph_sfh_lighting.json b/honeybee_ph_standards/schedules/hbph_sfh_lighting.json new file mode 100644 index 0000000..8555a1b --- /dev/null +++ b/honeybee_ph_standards/schedules/hbph_sfh_lighting.json @@ -0,0 +1,112 @@ +[ + { + "identifier": "hbph_sfh_Lighting", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_Lighting_Day Schedule", + "day_schedules": [ + { + "values": [ + 0.0080000000000000002, + 0.024, + 0.050000000000000003, + 0.056000000000000001, + 0.050000000000000003, + 0.021999999999999999, + 0.014999999999999999, + 0.025999999999999999, + 0.014999999999999999, + 0.056000000000000001, + 0.078, + 0.105, + 0.126, + 0.128, + 0.087999999999999995, + 0.049000000000000002, + 0.02 + ], + "interpolate": false, + "identifier": "hbph_sfh_Lighting_Day Schedule", + "times": [ + [ + 0, + 0 + ], + [ + 4, + 0 + ], + [ + 5, + 0 + ], + [ + 6, + 0 + ], + [ + 7, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 14, + 0 + ], + [ + 15, + 0 + ], + [ + 16, + 0 + ], + [ + 17, + 0 + ], + [ + 18, + 0 + ], + [ + 19, + 0 + ], + [ + 20, + 0 + ], + [ + 21, + 0 + ], + [ + 22, + 0 + ], + [ + 23, + 0 + ] + ], + "type": "ScheduleDay" + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "lower_limit": 0.0, + "unit_type": "Dimensionless", + "identifier": "Fractional", + "type": "ScheduleTypeLimit" + }, + "properties": {} + } +] \ No newline at end of file diff --git a/honeybee_ph_standards/schedules/hbph_sfh_occupancy.json b/honeybee_ph_standards/schedules/hbph_sfh_occupancy.json new file mode 100644 index 0000000..a3ade2a --- /dev/null +++ b/honeybee_ph_standards/schedules/hbph_sfh_occupancy.json @@ -0,0 +1,94 @@ +[ + { + "identifier": "hbph_sfh_Occupant_Presence", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_Occupant_Presence_Day Schedule", + "day_schedules": [ + { + "values": [ + 1.0, + 0.90000000000000002, + 0.40000000000000002, + 0.25, + 0.55000000000000004, + 0.90000000000000002, + 1.0 + ], + "interpolate": false, + "identifier": "hbph_sfh_Occupant_Presence_Day Schedule", + "times": [ + [ + 0, + 0 + ], + [ + 8, + 0 + ], + [ + 9, + 0 + ], + [ + 10, + 0 + ], + [ + 17, + 0 + ], + [ + 18, + 0 + ], + [ + 21, + 0 + ] + ], + "type": "ScheduleDay" + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": 1.0, + "lower_limit": 0.0, + "unit_type": "Dimensionless", + "identifier": "Fractional", + "type": "ScheduleTypeLimit" + }, + "properties": {} + }, + { + "identifier": "hbph_sfh_Occupant_Activity", + "type": "ScheduleRuleset", + "default_day_schedule": "hbph_sfh_Occupant_Activity_Day Schedule", + "day_schedules": [ + { + "values": [ + 112.53934481000194 + ], + "interpolate": false, + "identifier": "hbph_sfh_Occupant_Activity_Day Schedule", + "times": [ + [ + 0, + 0 + ] + ], + "type": "ScheduleDay" + } + ], + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": { + "type": "NoLimit" + }, + "lower_limit": 0.0, + "unit_type": "ActivityLevel", + "identifier": "Activity Level", + "type": "ScheduleTypeLimit" + }, + "properties": {} + } +] \ No newline at end of file diff --git a/honeybee_ph_standards/schedules/hbph_sfh_setpoint.json b/honeybee_ph_standards/schedules/hbph_sfh_setpoint.json new file mode 100644 index 0000000..026bebe --- /dev/null +++ b/honeybee_ph_standards/schedules/hbph_sfh_setpoint.json @@ -0,0 +1,68 @@ +[ + { + "default_day_schedule": "hbph_sfh_Cooling_Setpoint_Day Schedule", + "day_schedules": [ + { + "values": [ + 25.0 + ], + "interpolate": false, + "identifier": "hbph_sfh_Cooling_Setpoint_Day Schedule", + "times": [ + [ + 0, + 0 + ] + ], + "type": "ScheduleDay" + } + ], + "properties": {}, + "identifier": "hbph_sfh_Cooling_Setpoint", + "display_name": "hbph_sfh_Cooling_Setpoint", + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": { + "type": "NoLimit" + }, + "lower_limit": -273.14999999999998, + "unit_type": "Temperature", + "identifier": "Temperature", + "type": "ScheduleTypeLimit" + }, + "type": "ScheduleRuleset" + }, + { + "default_day_schedule": "hbph_sfh_Heating_Setpoint_Day Schedule", + "day_schedules": [ + { + "values": [ + 20.0 + ], + "interpolate": false, + "identifier": "hbph_sfh_Heating_Setpoint_Day Schedule", + "times": [ + [ + 0, + 0 + ] + ], + "type": "ScheduleDay" + } + ], + "properties": {}, + "identifier": "hbph_sfh_Heating_Setpoint", + "display_name": "hbph_sfh_Heating_Setpoint", + "schedule_type_limit": { + "numeric_type": "Continuous", + "upper_limit": { + "type": "NoLimit" + }, + "lower_limit": -273.14999999999998, + "unit_type": "Temperature", + "identifier": "Temperature", + "type": "ScheduleTypeLimit" + }, + "type": "ScheduleRuleset" + } +] \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 06c5e31..0ba779c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ source = [ "honeybee_ph_utils", "honeybee_phhvac", ] -# --- branch = true cover_pylib = false omit = ["*/__init__.py", "*.venv/*", "*.venv_mbair/*"] @@ -28,6 +27,7 @@ fail_under = 100 [tool.coverage.html] directory = "_coverage_html" + [tool.black] line-length = 120 diff --git a/requirements.txt b/requirements.txt index 8c4cf8c..d58f6d6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ honeybee-core>=1.61.1 -honeybee-energy>=1.112.3 +honeybee-energy>=1.112.5 ladybug-rhino>=1.43.5 PH-units>=1.5.18 plotly>=5.24.1 \ No newline at end of file diff --git a/tests/test_honeybee_energy_ph/test_load/test_ph_equipment.py b/tests/test_honeybee_energy_ph/test_load/test_ph_equipment.py index c1145e5..069193e 100644 --- a/tests/test_honeybee_energy_ph/test_load/test_ph_equipment.py +++ b/tests/test_honeybee_energy_ph/test_load/test_ph_equipment.py @@ -1,3 +1,5 @@ +from pytest import approx +from honeybee_energy.lib.schedules import schedule_by_identifier from honeybee_energy_ph.load import ph_equipment # -- Basics @@ -10,7 +12,6 @@ def test_PhDishwasher_round_trip(): assert e2.to_dict() == d1 - def test_PhClothesWasher_round_trip(): e1 = ph_equipment.PhClothesWasher() d1 = e1.to_dict() @@ -18,7 +19,6 @@ def test_PhClothesWasher_round_trip(): assert e2.to_dict() == d1 - def test_PhClothesDryer_round_trip(): e1 = ph_equipment.PhClothesDryer() d1 = e1.to_dict() @@ -26,7 +26,6 @@ def test_PhClothesDryer_round_trip(): assert e2.to_dict() == d1 - def test_PhRefrigerator_round_trip(): e1 = ph_equipment.PhRefrigerator() d1 = e1.to_dict() @@ -34,7 +33,6 @@ def test_PhRefrigerator_round_trip(): assert e2.to_dict() == d1 - def test_PhFreezer_round_trip(): e1 = ph_equipment.PhFreezer() d1 = e1.to_dict() @@ -42,7 +40,6 @@ def test_PhFreezer_round_trip(): assert e2.to_dict() == d1 - def test_PhFridgeFreezer_round_trip(): e1 = ph_equipment.PhFridgeFreezer() d1 = e1.to_dict() @@ -50,7 +47,6 @@ def test_PhFridgeFreezer_round_trip(): assert e2.to_dict() == d1 - def test_PhCooktop_round_trip(): e1 = ph_equipment.PhCooktop() d1 = e1.to_dict() @@ -69,7 +65,6 @@ def test_PhPhiusMEL_round_trip(): assert e2.to_dict() == d1 - def test_PhPhiusLightingInterior_round_trip(): e1 = ph_equipment.PhPhiusLightingInterior() d1 = e1.to_dict() @@ -77,7 +72,6 @@ def test_PhPhiusLightingInterior_round_trip(): assert e2.to_dict() == d1 - def test_PhPhiusLightingExterior_round_trip(): e1 = ph_equipment.PhPhiusLightingExterior() d1 = e1.to_dict() @@ -86,7 +80,6 @@ def test_PhPhiusLightingExterior_round_trip(): assert e2.to_dict() == d1 assert e2.in_conditioned_space == False - def test_PhPhiusLightingGarage_round_trip(): e1 = ph_equipment.PhPhiusLightingGarage() d1 = e1.to_dict() @@ -95,7 +88,6 @@ def test_PhPhiusLightingGarage_round_trip(): assert e2.to_dict() == d1 assert e2.in_conditioned_space == False - def test_PhCustomAnnualElectric_round_trip(): e1 = ph_equipment.PhCustomAnnualElectric() d1 = e1.to_dict() @@ -103,7 +95,6 @@ def test_PhCustomAnnualElectric_round_trip(): assert e2.to_dict() == d1 - def test_PhCustomAnnualLighting_round_trip(): e1 = ph_equipment.PhCustomAnnualLighting() d1 = e1.to_dict() @@ -111,7 +102,6 @@ def test_PhCustomAnnualLighting_round_trip(): assert e2.to_dict() == d1 - def test_PhCustomAnnualMEL_round_trip(): e1 = ph_equipment.PhCustomAnnualMEL() d1 = e1.to_dict() @@ -130,7 +120,6 @@ def test_PhElevatorHydraulic_round_trip(): assert e2.to_dict() == d1 - def test_PhElevatorGearedTraction_round_trip(): e1 = ph_equipment.PhElevatorGearedTraction() d1 = e1.to_dict() @@ -138,10 +127,281 @@ def test_PhElevatorGearedTraction_round_trip(): assert e2.to_dict() == d1 - def test_PhElevatorGearlessTraction_round_trip(): e1 = ph_equipment.PhElevatorGearlessTraction() d1 = e1.to_dict() e2 = ph_equipment.PhEquipmentBuilder.from_dict(d1) assert e2.to_dict() == d1 + + +# -- Defaults + + +def _test_defaults(_type): + phi_equip =_type.phi_default() + assert phi_equip.comment == "default" + + d = phi_equip.to_dict() + e = ph_equipment.PhEquipmentBuilder.from_dict(d) + assert e.to_dict() == d + + phius_equip =_type.phius_default() + assert phius_equip.comment == "default" + + d = phius_equip.to_dict() + e = ph_equipment.PhEquipmentBuilder.from_dict(d) + assert e.to_dict() == d + +def test_dishwasher_default(): + _test_defaults(ph_equipment.PhDishwasher) + +def test_clothes_washer_default(): + _test_defaults(ph_equipment.PhClothesWasher) + +def test_clothes_dryer_default(): + _test_defaults(ph_equipment.PhClothesDryer) + +def test_refrigerator_default(): + _test_defaults(ph_equipment.PhRefrigerator) + +def test_freezer_default(): + _test_defaults(ph_equipment.PhFreezer) + +def test_fridge_freezer_default(): + _test_defaults(ph_equipment.PhFridgeFreezer) + +def test_cooktop_default(): + _test_defaults(ph_equipment.PhCooktop) + +def test_phius_mel_default(): + _test_defaults(ph_equipment.PhPhiusMEL) + +def test_phius_lighting_interior_default(): + _test_defaults(ph_equipment.PhPhiusLightingInterior) + +def test_phius_lighting_exterior_default(): + _test_defaults(ph_equipment.PhPhiusLightingExterior) + +def test_phius_lighting_garage_default(): + _test_defaults(ph_equipment.PhPhiusLightingGarage) + +def test_custom_annual_electric_default(): + _test_defaults(ph_equipment.PhCustomAnnualElectric) + +def test_custom_annual_lighting_default(): + _test_defaults(ph_equipment.PhCustomAnnualLighting) + +def test_custom_annual_mel_default(): + _test_defaults(ph_equipment.PhCustomAnnualMEL) + + +# -- Annual Energy kWH + + +def test_dishwasher_annual_kWh(): + e = ph_equipment.PhDishwasher.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(269) + +def test_clothes_washer_annual_kWh(): + e = ph_equipment.PhClothesWasher.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(120) + +def test_clothes_dryer_annual_kWh(): + e = ph_equipment.PhClothesDryer.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(405.657336726039) + +def test_refrigerator_annual_kWh(): + e = ph_equipment.PhRefrigerator.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(365.0) + +def test_freezer_annual_kWh(): + e = ph_equipment.PhFreezer.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(394.0175) + +def test_fridge_freezer_annual_kWh(): + e = ph_equipment.PhFridgeFreezer.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(445.3) + +def test_cooktop_annual_kWh(): + e = ph_equipment.PhCooktop.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(300.0) + +def test_phius_mel_annual_kWh(): + e = ph_equipment.PhPhiusMEL.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(1168.8) + +def test_phius_lighting_interior_annual_kWh(): + e = ph_equipment.PhPhiusLightingInterior.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(417.881081081081) + +def test_phius_lighting_exterior_annual_kWh(): + e = ph_equipment.PhPhiusLightingExterior.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(30.0) + +def test_phius_lighting_garage_annual_kWh(): + e = ph_equipment.PhPhiusLightingGarage.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(20.0) + +def test_custom_annual_electric_annual_kWh(): + e = ph_equipment.PhCustomAnnualElectric.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(0) + +def test_custom_annual_lighting_annual_kWh(): + e = ph_equipment.PhCustomAnnualLighting.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(0) + +def test_custom_annual_mel_annual_kWh(): + e = ph_equipment.PhCustomAnnualMEL.phius_default() + annual_kwh = e.annual_energy_kWh( + **{ + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + } + ) + assert annual_kwh == approx(0) + + +# -- Annual Average Wattage + + +def test_average_wattage(): + d = { + '_num_occupants': 3, + '_num_bedrooms': 2, + '_floor_area_ft2': 1_000, + '_schedule': schedule_by_identifier("Always On"), + } + e = ph_equipment.PhDishwasher.phius_default() + assert e.annual_avg_wattage(**d) == approx(30.707762557077626) + + e = ph_equipment.PhClothesWasher.phius_default() + assert e.annual_avg_wattage(**d) == approx(13.698630136986301) + + e = ph_equipment.PhClothesDryer.phius_default() + assert e.annual_avg_wattage(**d) == approx(46.30791515137431) + + e = ph_equipment.PhRefrigerator.phius_default() + assert e.annual_avg_wattage(**d) == approx(41.666666666666664) + + e = ph_equipment.PhFreezer.phius_default() + assert e.annual_avg_wattage(**d) == approx(44.979166666666664) + + e = ph_equipment.PhFridgeFreezer.phius_default() + assert e.annual_avg_wattage(**d) == approx(50.833333333333336) + + e = ph_equipment.PhCooktop.phius_default() + assert e.annual_avg_wattage(**d) == approx(34.24657534246575) + + e = ph_equipment.PhPhiusMEL.phius_default() + assert e.annual_avg_wattage(**d) == approx(133.42465753424656) + + e = ph_equipment.PhPhiusLightingInterior.phius_default() + assert e.annual_avg_wattage(**d) == approx(47.70331975811428) + + e = ph_equipment.PhPhiusLightingExterior.phius_default() + assert e.annual_avg_wattage(**d) == approx(3.4246575342465753) + + e = ph_equipment.PhPhiusLightingGarage.phius_default() + assert e.annual_avg_wattage(**d) == approx(2.28310502283105) + + e = ph_equipment.PhCustomAnnualElectric.phius_default() + assert e.annual_avg_wattage(**d) == approx(0) + + e = ph_equipment.PhCustomAnnualLighting.phius_default() + assert e.annual_avg_wattage(**d) == approx(0) + + e = ph_equipment.PhCustomAnnualMEL.phius_default() + assert e.annual_avg_wattage(**d) == approx(0) \ No newline at end of file