diff --git a/rct229/rulesets/ashrae9012022/__init__.py b/rct229/rulesets/ashrae9012022/__init__.py index befd2e8036..f1c82847ea 100644 --- a/rct229/rulesets/ashrae9012022/__init__.py +++ b/rct229/rulesets/ashrae9012022/__init__.py @@ -9,8 +9,8 @@ __all__ = [ "section5", "section6", + "section12", "section21", - "SHORT_NAME", "BASELINE_0", "BASELINE_90", "BASELINE_180", @@ -69,6 +69,7 @@ "prm9012022rule23o29": "section6rule11", "prm9012022rule12d80": "section6rule12", "prm9012022rule86d29": "section6rule13", + "prm9012022rule23z21": "section12rule5", "prm9012022rule93e12": "section21rule19", } @@ -76,12 +77,14 @@ "All", "Envelope", "Lighting", + "Receptacles", "HVAC-HotWaterSide", ] section_dict = { "5": "Envelope", "6": "Lighting", + "12": "Receptacles", "21": "HVAC-HotWaterSide", } diff --git a/rct229/rulesets/ashrae9012022/section12/__init__.py b/rct229/rulesets/ashrae9012022/section12/__init__.py new file mode 100644 index 0000000000..df71929c80 --- /dev/null +++ b/rct229/rulesets/ashrae9012022/section12/__init__.py @@ -0,0 +1,16 @@ +# Add all available rule modules in __all__ +import importlib + +__all__ = [ + "section12rule5", +] + + +def __getattr__(name): + if name in __all__: + return importlib.import_module("." + name, __name__) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +def __dir__(): + return sorted(__all__) diff --git a/rct229/rulesets/ashrae9012022/section12/section12rule5.py b/rct229/rulesets/ashrae9012022/section12/section12rule5.py new file mode 100644 index 0000000000..34bb173865 --- /dev/null +++ b/rct229/rulesets/ashrae9012022/section12/section12rule5.py @@ -0,0 +1,236 @@ +from pydash import flatten +from rct229.rule_engine.rule_base import RuleDefinitionBase +from rct229.rule_engine.rule_list_indexed_base import RuleDefinitionListIndexedBase +from rct229.rule_engine.ruleset_model_factory import produce_ruleset_model_description +from rct229.rulesets.ashrae9012019 import PROPOSED +from rct229.schema.config import ureg +from rct229.schema.schema_enums import SchemaEnums +from rct229.utils.assertions import getattr_ +from rct229.utils.jsonpath_utils import find_all +from rct229.utils.schedule_utils import get_schedule_multiplier_hourly_value_or_default + +END_USE = SchemaEnums.schema_enums["EndUseOptions"] + +ACCEPTABLE_RESULT_TYPE = [ + END_USE.MISC_EQUIPMENT, + END_USE.INDUSTRIAL_PROCESS, + END_USE.OFFICE_EQUIPMENT, + END_USE.COMPUTERS_SERVERS, + END_USE.COMMERCIAL_COOKING, +] + + +class PRM9012022Rule23z21(RuleDefinitionListIndexedBase): + """Rule 5 of ASHRAE 90.1-2022 Appendix G Section 12 (Receptacle)""" + + def __init__(self): + super(PRM9012022Rule23z21, self).__init__( + rmds_used=produce_ruleset_model_description( + USER=False, BASELINE_0=True, PROPOSED=True + ), + each_rule=PRM9012022Rule23z21.RMDRule(), + index_rmd=PROPOSED, + id="12-5", + description="Receptacle and process loads shall always be included in simulations of the building. " + "These loads shall be included when calculating the proposed building performance and the baseline building performance as required by Section G1.2.1.", + ruleset_section_title="Receptacle", + standard_section="Table G3.1-12 Proposed Building Performance column", + is_primary_rule=True, + list_path="ruleset_model_descriptions[0]", + ) + + class RMDRule(RuleDefinitionListIndexedBase): + def __init__(self): + super(PRM9012022Rule23z21.RMDRule, self).__init__( + rmds_used=produce_ruleset_model_description( + USER=False, BASELINE_0=True, PROPOSED=True + ), + each_rule=PRM9012022Rule23z21.RMDRule.MiscellaneousEquipmentRule(), + index_rmd=PROPOSED, + list_path="buildings[*].building_segments[*].zones[*].spaces[*].miscellaneous_equipment[*]", + ) + + def create_data(self, context, data): + rmd_b = context.BASELINE_0 + rmd_p = context.PROPOSED + + schedule_eflh_b = sum( + flatten( + [ + get_schedule_multiplier_hourly_value_or_default( + rmd_b, + getattr_( + misc_equip_b, + "miscellaneous_equipment", + "multiplier_schedule", + ), + ) + for misc_equip_b in find_all( + "$.buildings[*].building_segments[*].zones[*].spaces[*].miscellaneous_equipment[*]", + rmd_b, + ) + ] + ), + 0, + ) + + schedule_eflh_p = sum( + flatten( + [ + get_schedule_multiplier_hourly_value_or_default( + rmd_p, + getattr_( + misc_equip_p, + "miscellaneous_equipment", + "multiplier_schedule", + ), + ) + for misc_equip_p in find_all( + "$.buildings[*].building_segments[*].zones[*].spaces[*].miscellaneous_equipment[*]", + rmd_p, + ) + ] + ), + 0, + ) + + has_annual_energy_use_b = any( + [ + getattr_(annual_end_use_result, "annual_end_use_results", "type") + in ACCEPTABLE_RESULT_TYPE + and getattr_( + annual_end_use_result, + "annual_end_use_results", + "annual_site_energy_use", + ) + > 0 * ureg("J") + for annual_end_use_result in find_all( + "$.model_output.annual_end_use_results[*]", + rmd_b, + ) + ] + ) + + has_annual_energy_use_p = any( + [ + getattr_(annual_end_use_result, "annual_end_use_results", "type") + in ACCEPTABLE_RESULT_TYPE + and getattr_( + annual_end_use_result, + "annual_end_use_results", + "annual_site_energy_use", + ) + > 0 * ureg("J") + for annual_end_use_result in find_all( + "$.model_output.annual_end_use_results[*]", + rmd_p, + ) + ] + ) + + return { + "schedule_eflh_b": schedule_eflh_b, + "schedule_eflh_p": schedule_eflh_p, + "has_annual_energy_use_b": has_annual_energy_use_b, + "has_annual_energy_use_p": has_annual_energy_use_p, + } + + class MiscellaneousEquipmentRule(RuleDefinitionBase): + def __init__(self): + super( + PRM9012022Rule23z21.RMDRule.MiscellaneousEquipmentRule, + self, + ).__init__( + rmds_used=produce_ruleset_model_description( + USER=False, BASELINE_0=True, PROPOSED=True + ), + required_fields={"$": ["power"]}, + ) + + def get_calc_vals(self, context, data=None): + misc_equip_b = context.BASELINE_0 + misc_equip_p = context.PROPOSED + + has_annual_energy_use_b = data["has_annual_energy_use_b"] + schedule_eflh_b = data["schedule_eflh_b"] + + has_annual_energy_use_p = data["has_annual_energy_use_p"] + schedule_eflh_p = data["schedule_eflh_p"] + + loads_included_b = ( + misc_equip_b["power"] > 0 * ureg("W") + and ( + misc_equip_b.get("sensible_fraction", 0) > 0 + or misc_equip_b.get("latent_fraction", 0) > 0 + ) + and schedule_eflh_b > 0 + ) + + loads_included_p = ( + misc_equip_p["power"] > 0 * ureg("W") + and ( + misc_equip_p.get("sensible_fraction", 0) > 0 + or misc_equip_p.get("latent_fraction", 0) > 0 + ) + and schedule_eflh_p > 0 + ) + + return { + "has_annual_energy_use_b": has_annual_energy_use_b, + "has_annual_energy_use_p": has_annual_energy_use_p, + "loads_included_b": loads_included_b, + "loads_included_p": loads_included_p, + } + + def rule_check(self, context, calc_vals=None, data=None): + loads_included_b = calc_vals["loads_included_b"] + loads_included_p = calc_vals["loads_included_p"] + has_annual_energy_use_b = calc_vals["has_annual_energy_use_b"] + has_annual_energy_use_p = calc_vals["has_annual_energy_use_p"] + + return ( + loads_included_b + and loads_included_p + and has_annual_energy_use_b + and has_annual_energy_use_p + ) + + def get_fail_msg(self, context, calc_vals=None, data=None): + misc_equip_b = context.BASELINE_0 + misc_equip_p = context.PROPOSED + loads_included_b = calc_vals["loads_included_b"] + loads_included_p = calc_vals["loads_included_p"] + has_annual_energy_use_b = calc_vals["has_annual_energy_use_b"] + has_annual_energy_use_p = calc_vals["has_annual_energy_use_p"] + schedule_eflh_b = data["schedule_eflh_b"] + schedule_eflh_p = data["schedule_eflh_p"] + + FAIL_MSG = " | ".join( + filter( + None, + [ + ( + f"Proposed: No misc. loads [power={misc_equip_p['power']}, sens={misc_equip_p.get('sensible_fraction')}, lat={misc_equip_p.get('latent_fraction')}, EFLH={schedule_eflh_p}]" + if not loads_included_p + else "" + ), + ( + "Proposed: No annual end use energy." + if not has_annual_energy_use_p + else "" + ), + ( + f"Baseline: No misc. loads [power={misc_equip_b['power']}, sens={misc_equip_b.get('sensible_fraction')}, lat={misc_equip_b.get('latent_fraction')}, EFLH={schedule_eflh_b}]" + if not loads_included_b + else "" + ), + ( + "Baseline: No annual end use energy." + if not has_annual_energy_use_b + else "" + ), + ], + ) + ) + + return FAIL_MSG