From ea9b2fe4d264adf8e4c877e009369e61847843b8 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Mon, 27 Oct 2025 16:54:00 -0700 Subject: [PATCH 1/4] 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/4] 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/4] 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 ) From 1dbcd3dba14ba3c451da95595b9b5ae27ec5a223 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Mon, 24 Nov 2025 16:09:29 -0800 Subject: [PATCH 4/4] Updated the rule logic (baseline) --- rct229/rulesets/ashrae9012022/__init__.py | 4 +- .../ashrae9012022/section12/section12rule5.py | 150 ++++++++++++++---- 2 files changed, 121 insertions(+), 33 deletions(-) diff --git a/rct229/rulesets/ashrae9012022/__init__.py b/rct229/rulesets/ashrae9012022/__init__.py index 53ae9e99bf..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", @@ -77,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/section12rule5.py b/rct229/rulesets/ashrae9012022/section12/section12rule5.py index f4e5987ca2..34bb173865 100644 --- a/rct229/rulesets/ashrae9012022/section12/section12rule5.py +++ b/rct229/rulesets/ashrae9012022/section12/section12rule5.py @@ -26,13 +26,13 @@ class PRM9012022Rule23z21(RuleDefinitionListIndexedBase): def __init__(self): super(PRM9012022Rule23z21, self).__init__( rmds_used=produce_ruleset_model_description( - USER=False, BASELINE_0=False, PROPOSED=True + USER=False, BASELINE_0=True, 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.", + 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, @@ -43,16 +43,37 @@ class RMDRule(RuleDefinitionListIndexedBase): def __init__(self): super(PRM9012022Rule23z21.RMDRule, self).__init__( rmds_used=produce_ruleset_model_description( - USER=False, BASELINE_0=False, PROPOSED=True + USER=False, BASELINE_0=True, PROPOSED=True ), each_rule=PRM9012022Rule23z21.RMDRule.MiscellaneousEquipmentRule(), index_rmd=PROPOSED, - list_path="buildings[*].building_segments[*].zones[*].spaces[*]", + 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( [ @@ -73,23 +94,44 @@ def create_data(self, context, data): 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, - ) + [ + 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, } @@ -100,51 +142,95 @@ def __init__(self): self, ).__init__( rmds_used=produce_ruleset_model_description( - USER=False, BASELINE_0=False, PROPOSED=True + USER=False, BASELINE_0=True, PROPOSED=True ), - required_fields={ - "$": ["power", "sensible_fraction", "latent_fraction"] - }, + 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["sensible_fraction"] > 0 - or misc_equip_p["latent_fraction"] > 0 + misc_equip_p.get("sensible_fraction", 0) > 0 + or misc_equip_p.get("latent_fraction", 0) > 0 ) and schedule_eflh_p > 0 ) return { - "loads_included_p": loads_included_p, + "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_p and 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 = "" - 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}'}" + 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 "" + ), + ], ) - if not has_annual_energy_use_p: - FAIL_MSG += " No annual end use energy is reported for the relevant equipment types." + ) return FAIL_MSG