From ea9b2fe4d264adf8e4c877e009369e61847843b8 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Mon, 27 Oct 2025 16:54:00 -0700 Subject: [PATCH 1/3] Developed 12-5 rule --- rct229/rulesets/ashrae9012022/__init__.py | 15 +- .../ashrae9012022/section12/__init__.py | 16 ++ .../ashrae9012022/section12/section12rule5.py | 141 ++++++++++++++++++ 3 files changed, 166 insertions(+), 6 deletions(-) create mode 100644 rct229/rulesets/ashrae9012022/section12/__init__.py create mode 100644 rct229/rulesets/ashrae9012022/section12/section12rule5.py diff --git a/rct229/rulesets/ashrae9012022/__init__.py b/rct229/rulesets/ashrae9012022/__init__.py index bd698afd61..e85882a2bb 100644 --- a/rct229/rulesets/ashrae9012022/__init__.py +++ b/rct229/rulesets/ashrae9012022/__init__.py @@ -5,26 +5,29 @@ from rct229.schema.schema_store import SchemaStore # Add all available rule modules in __all__ -__all__ = ["section5", "section6", "section21"] +__all__ = ["section5", "section6", "section12", "section21"] rules_dict = { - "PRM9012022Rule86r63": "section5rule43", - "PRM9012022Rule13d92": "section5rule44", - "PRM9012022Rule22f12": "section5rule45", - "PRM9012022Rule12d80": "section6rule11", - "PRM9012022Rule93e12": "section21rule19", + "PRM9012022rule86r63": "section5rule43", + "PRM9012022rule13d92": "section5rule44", + "PRM9012022rule22f12": "section5rule45", + "PRM9012022rule12d80": "section6rule11", + "PRM9012022rule23z21": "section12rule5", + "PRM9012022rule93e12": "section21rule19", } section_list = [ "Env", "LTG", + "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..c57650eda5 --- /dev/null +++ b/rct229/rulesets/ashrae9012022/section12/section12rule5.py @@ -0,0 +1,141 @@ +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=False, PROPOSED=True + ), + each_rule=PRM9012022rule23z21.RMDRule(), + index_rmd=PROPOSED, + id="12-5", + description="hese 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=False, PROPOSED=True + ), + each_rule=PRM9012022rule23z21.RMDRule.MiscellaneousEquipmentRule(), + index_rmd=PROPOSED, + list_path="buildings[*].building_segments[*].zones[*].spaces[*]", + ) + + def create_data(self, context, data): + rmd_p = context.PROPOSED + + schedule_eflh_p = sum( + [ + 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], + 0, + ) + + 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_p": schedule_eflh_p, + "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=False, PROPOSED=True + ), + required_fields={ + "$": ["power", "sensible_fraction", "latent_fraction"] + }, + ) + + def get_calc_vals(self, context, data=None): + misc_equip_p = context.PROPOSED + has_annual_energy_use_p = data["has_annual_energy_use_p"] + schedule_eflh_p = data["schedule_eflh_p"] + + loads_included_p = ( + misc_equip_p["power"] > 0 * ureg("W") + and misc_equip_p["sensible_fraction"] > 0 + and misc_equip_p["latent_fraction"] > 0 + and schedule_eflh_p > 0 + ) + + return { + "loads_included_p": loads_included_p, + "has_annual_energy_use_p": has_annual_energy_use_p, + } + + def rule_check(self, context, calc_vals=None, data=None): + loads_included_p = calc_vals["loads_included_p"] + has_annual_energy_use_p = calc_vals["has_annual_energy_use_p"] + + return loads_included_p and has_annual_energy_use_p + + def get_fail_msg(self, context, calc_vals=None, data=None): + misc_equip_p = context.PROPOSED + has_annual_energy_use_p = calc_vals["has_annual_energy_use_p"] + loads_included_p = calc_vals["loads_included_p"] + schedule_eflh_p = data["schedule_eflh_p"] + + FAIL_MSG = ( + f"No miscellaneous equipment loads are included. [power: {misc_equip_p['power']}, sensible_fraction: {misc_equip_p['sensible_fraction']}, " + f"latent_fraction: {misc_equip_p['latent_fraction']}, schedule_eflh: {schedule_eflh_p}] {'No annual end use energy is reported for the relevant equipment types. {has_annual_energy_use_p_msg}'}" + ) # Need to double-check the message + + return FAIL_MSG From 654bed1d9280186d3555877008cb6551166ea377 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Tue, 28 Oct 2025 08:43:51 -0700 Subject: [PATCH 2/3] Updated failed msg --- .../ashrae9012022/section12/section12rule5.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/rct229/rulesets/ashrae9012022/section12/section12rule5.py b/rct229/rulesets/ashrae9012022/section12/section12rule5.py index c57650eda5..63cc28bd90 100644 --- a/rct229/rulesets/ashrae9012022/section12/section12rule5.py +++ b/rct229/rulesets/ashrae9012022/section12/section12rule5.py @@ -129,13 +129,17 @@ def rule_check(self, context, calc_vals=None, data=None): def get_fail_msg(self, context, calc_vals=None, data=None): misc_equip_p = context.PROPOSED - has_annual_energy_use_p = calc_vals["has_annual_energy_use_p"] loads_included_p = calc_vals["loads_included_p"] + has_annual_energy_use_p = calc_vals["has_annual_energy_use_p"] schedule_eflh_p = data["schedule_eflh_p"] - FAIL_MSG = ( - f"No miscellaneous equipment loads are included. [power: {misc_equip_p['power']}, sensible_fraction: {misc_equip_p['sensible_fraction']}, " - f"latent_fraction: {misc_equip_p['latent_fraction']}, schedule_eflh: {schedule_eflh_p}] {'No annual end use energy is reported for the relevant equipment types. {has_annual_energy_use_p_msg}'}" - ) # Need to double-check the message + FAIL_MSG = "" + if not loads_included_p: + FAIL_MSG = ( + f"No miscellaneous equipment loads are included. [power: {misc_equip_p['power']}, sensible_fraction: {misc_equip_p['sensible_fraction']}, " + f"latent_fraction: {misc_equip_p['latent_fraction']}, schedule_eflh: {schedule_eflh_p}] {'No annual end use energy is reported for the relevant equipment types. {has_annual_energy_use_p_msg}'}" + ) + if not has_annual_energy_use_p: + FAIL_MSG += " No annual end use energy is reported for the relevant equipment types." return FAIL_MSG From d4579b50fe70968d49dc8ca50e72a9715db543f3 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Tue, 18 Nov 2025 09:42:23 -0800 Subject: [PATCH 3/3] Addressed PR comments --- .../ashrae9012022/section12/section12rule5.py | 37 +++++++++++-------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/rct229/rulesets/ashrae9012022/section12/section12rule5.py b/rct229/rulesets/ashrae9012022/section12/section12rule5.py index 10d9f63dbc..f4e5987ca2 100644 --- a/rct229/rulesets/ashrae9012022/section12/section12rule5.py +++ b/rct229/rulesets/ashrae9012022/section12/section12rule5.py @@ -1,3 +1,4 @@ +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 @@ -53,20 +54,22 @@ def create_data(self, context, data): rmd_p = context.PROPOSED schedule_eflh_p = sum( - [ - 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], + 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, ) @@ -111,8 +114,10 @@ def get_calc_vals(self, context, data=None): loads_included_p = ( misc_equip_p["power"] > 0 * ureg("W") - and misc_equip_p["sensible_fraction"] > 0 - and misc_equip_p["latent_fraction"] > 0 + and ( + misc_equip_p["sensible_fraction"] > 0 + or misc_equip_p["latent_fraction"] > 0 + ) and schedule_eflh_p > 0 )