From 148bfa1f0314f319f5571cfa4640c65db0aa553c Mon Sep 17 00:00:00 2001 From: Jackson Jarboe <122476654+JacksonJ-KC@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:16:50 -0400 Subject: [PATCH 1/8] update more complete logic --- docs/section5/Rule5-1.md | 50 ++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 27 deletions(-) diff --git a/docs/section5/Rule5-1.md b/docs/section5/Rule5-1.md index 1caa90f738..2469bb3a06 100644 --- a/docs/section5/Rule5-1.md +++ b/docs/section5/Rule5-1.md @@ -2,7 +2,7 @@ **Schema Version** 0.0.33 **Primary Rule** True **Rule ID:** 5-1 -**Rule Description:** There are four baseline rotations (i.e., four baseline models differing in azimuth by 90 degrees and four sets of baseline model results) if vertical fenestration area per each orientation differ by more than 5%. +**Rule Description:** The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. Exceptions: 1. If it can be demonstrated to the satisfaction of the rat-ing authority that the building orientation is dictated bysite considerations. 2. Buildings where the vertical fenestration area on eachorientation varies by less than 5%. **Rule Assertion:** Options are PASS/FAIL **Appendix G Section:** Table G3.1#5a baseline column **90.1 Section Reference:** None @@ -20,7 +20,7 @@ None ## Rule Logic: -Get the fenestration area for each unique orientation (i.e., azimuth) and then check if the minimum and maximum areas differ by 5% or more +Get the fenestration area for each unique orientation. - Create a blank dictionary that will have all unique azimuths paired with the total vertical fenestration area: `azimuth_fen_area_dict_b = {}` - For each building in the B_RMD: `for bldg in B_RMD.buildings:` - For each building_segment in the bldg: `for bldg_seg in bldg.building_segments` @@ -37,8 +37,9 @@ Get the fenestration area for each unique orientation (i.e., azimuth) and then c - Else, subsurface is not door, add total area to total_surface_fenestration_area (because we checked that the surface is an above grade wall we can assume that the subsurface classification is not and could not be a skylight so no need to check this): `total_surface_fenestration_area += subsurface.glazed_area + subsurface.opaque_area` - Add the total_surface_fenestration_area summed for the surface to the total fen area associated with the azimuth: `azimuth_fen_area_dict_b[surface_azimuth] += total_surface_fenestration_area` -- Loop through the dictionary keys and put the area in bins depending on the azimuth (bins will be in 3 degree increments). Use this logic: if azimuth >= 0 and < 3 then put area in the 0-3 bin, if azimuth >=3 and < 6 then put the area in the the 3-6 bin, etc. `for azi in azimuth_fen_area_dict_b.keys():` - - Lookup the bin that the azimuth falls into based on the value of the azimuth using this logic. Bin lookup table based on this logic: if azimuth >= 0 and < 3 then put the fen area in the 0-3 bin, if azimuth >=3 and < 6 then put the area in the the 3-6 bin, etc. (this assumes the RCT team will create a lookup table to make it easy to lookup the bin that the azimuth (azi) falls into): `bin = lookup(azi, lookuptable)` +Put the areas in bins depending on the azimuth (bins will be in 3 degree increments). Use this logic: if azimuth >= 0 and < 3 then put area in the 0-3 bin, if azimuth >=3 and < 6 then put the area in the 3-6 bin, etc. +- Loop through the dictionary keys: `for azi in azimuth_fen_area_dict_b.keys():` + - Lookup the bin that the azimuth falls into based on the value of the azimuth using this logic. Bin lookup table based on this logic: if azimuth >= 0 and < 3 then put the fen area in the 0-3 bin, if azimuth >=3 and < 6 then put the area in the 3-6 bin, etc. (this assumes the RCT team will create a lookup table to make it easy to lookup the bin that the azimuth (azi) falls into): `bin = lookup(azi, lookuptable)` - Add the area to the bin in a revised binned dictionary (bin is key, area is value in dictionary): `azimuth_fen_area_dict_b[bin] += azimuth_fen_area_dict_b[azi]` Check if the area differs by 5 percent or more. @@ -48,34 +49,29 @@ Check if the area differs by 5 percent or more. - Check if the % difference is 5% or more, if it is then set rotation_expected_b boolean to TRUE: `if percent_difference >= 5%: rotation_expected_b = TRUE` - Else, set rotation_expected_b to FALSE: `else: rotation_expected_b = FALSE` -- Set no_of_output_instance variable to 0 (counts the number of output instances): `no_of_output_instance =0` -Determine which RMDs have been created/provided -- Create variable for list of RMDs: `rmds = RulesetProjectDescription.ruleset_model_descriptions` -- Check for user RMD: `has_user = any[rmd.type == USER for rmd in rmds]` -- Check for proposed RMD: `has_proposed = any[rmd.type == PROPOSED for rmd in rmds]` -- Check for baseline 0 degree RMD: `has_baseline_0 = any[rmd.type == BASELINE_0 for rmd in rmds]` -- Check for baseline 90 degree RMD: `has_baseline_90 = any[rmd.type == BASELINE_90 for rmd in rmds]` -- Check for baseline 180 degree RMD: `has_baseline_180 = any[rmd.type == BASELINE_180 for rmd in rmds]` -- Check for baseline 270 degree RMD: `has_baseline_270 = any[rmd.type == BASELINE_270 for rmd in rmds]` -- Get the number of ruleset_model_descriptions: `no_of_rmds = len(rmds)` - -- Check that there is an output_instance associated with each RMD and add to the no_of_output_instance variable for each one `For rmd in rmds:` - - Check for proposed output: `if rmd.type == PROPOSED and rmd.output.Output2019ASHRAE901.output_instance != Null: has_proposed_output = TRUE` - - Check for baseline 0 degree output: `if rmd.type == BASELINE_0 and rmd.output.Output2019ASHRAE901.output_instance != Null: has_baseline_0_output = TRUE` - - Check for baseline 90 degree output: `if rmd.type == BASELINE_90 and rmd.output.Output2019ASHRAE901.output_instance != Null: has_baseline_90_output = TRUE` - - Check for baseline 180 degree output: `if rmd.type == BASELINE_180 and rmd.output.Output2019ASHRAE901.output_instance != Null: has_baseline_180_output = TRUE` - - Check for baseline 270 degree output: `if rmd.type == BASELINE_270 and rmd.output.Output2019ASHRAE901.output_instance != Null: has_baseline_270_output = TRUE` - - Check if there is an output_instance associated with the RMD: `if rmd.output.Output2019ASHRAE901.output_instance != Null: no_of_output_instance += 1 ` +Check the given BBP and calculate the expected BBP based on the output source results annual cost from the baseline RPD(s). +- Keep a set to store unique values of baseline building performance energy cost: `bbp_values = set()` +- Get the total annual cost for all source results in the baseline 0 RPD: `baseline_0_total_annual_cost = sum(rpd_b0.output.source_results.annual_cost)` +- Keep a list to store the total annual cost for baseline rotation RPDs (if they exist): `baseline_rotation_total_annual_costs = []` +- Iterate through the RPDs: `for rpd in (rpd_u, rpd_b0, rpd_b90, rpd_b180, rpd_b270, rpd_p):` + - If the RPD exists, check the output baseline building performance energy cost and add it to the set: `if rpd != Null: bbp_values.add(rpd.output.baseline_building_performance_energy_cost)` + - If the RPD exists, and it is a baseline rotation RPD, add the total annual cost to the list: `if rpd != Null and rpd.type in [BASELINE_90, BASELINE_180, BASELINE_270]: baseline_rotation_total_annual_costs.append(sum(rpd.output.source_results.annual_cost))` +- If the length of the set is greater than one, exit evaluation early and return UNDETERMINED with a message: `if len(bbp_values) > 1: UNDETERMINED raise_message "Ruleset expects exactly one BBP value to be used in the project."` +- Get the single BBP value from the set: `bbp = bbp_values.pop()` +- Calculate the average of the total annual costs for the baseline RPDs: `avg_baseline_total_annual_cost = (sum(baseline_rotation_total_annual_costs) + baseline_0_total_annual_cost) / (len(baseline_rotation_total_annual_costs) + 1)` -- **Rule Assertion:** -- Case 1: If the fenestration area differs by 5% or more by orientation and there are 6 RMDs (for user, proposed, baseline at 0 degrees, baseline at 90 degrees, baseline at 180 degrees, and baseline at 270 degrees) and 5 output files (excludes an output for the user model) then Pass: `if rotation_expected_b == TRUE and has_user == TRUE and has_proposed == TRUE and has_baseline_0 == TRUE and has_baseline_90 == TRUE and has_baseline_180 == TRUE and has_baseline_270 == TRUE and has_proposed_output == TRUE and has_baseline_0_output == TRUE and has_baseline_90_output == TRUE and has_baseline_180_output == TRUE and has_baseline_270_output == TRUE and no_of_rmds == 6 and no_of_output_instance == 5: outcome = "PASS" ` -- Case 2: Else if rotation is not expected then pass as long as they have the minimally required RMDs and outputs: `if rotation_expected_b == FALSE and has_user == TRUE and has_proposed == TRUE and has_baseline_0 == TRUE and has_proposed_output == TRUE and has_baseline_0_output == TRUE: outcome = "PASS" ` -- Case 43: Else: `Else: outcome = "FAIL" and raise_message "Fail unless Table G3.1#5a exception #2 is applicable and it can be demonstrated that the building orientation is dictated by site considerations.` +- **Rule Assertion:** +- Case 1: Baseline rotations are expected based on the fenestration areas by azimuth and the baseline building performance matches the average of the total source results annual cost from the 4 baseline RMDs: PASS: `if rotation_expected_b and bbp == avg_baseline_total_annual_cost: PASS` +- Case 2: Baseline rotations are not expected based on the fenestration areas by azimuth and the baseline building performance matches the total source results annual cost from the BASELINE_0 RMD: PASS: `elif !rotation_expected_b and bbp == baseline_0_total_annual_cost: PASS` +- Case 3: Baseline rotations are not expected based on the fenestration areas by azimuth and the baseline building performance does not match the total source results annual cost from the BASELINE_0 RMD: FAIL: `elif !rotation_expected_b and bbp != baseline_0_total_annual_cost: FAIL` +- Case 4: Baseline rotations are expected based on the fenestration areas by azimuth and the baseline building performance does not match either the average of the total source results annual cost from the 4 baseline RMDs or the BASELINE_0 RMD: FAIL: `elif rotation_expected_b and bbp != avg_baseline_total_annual_cost and bbp != baseline_0_total_annual_cost: FAIL` +- Case 5: Baseline rotations are expected based on the fenestration areas by azimuth and the baseline building performance matches the total source results annual cost from the BASELINE_0 RMD: UNDETERMINED "Perform a manual check to verify whether Table G3.1#5a exception #2 is applicable, and it can be demonstrated that the building orientation is dictated by site considerations.": `elif rotation_expected_b and bbp == baseline_0_total_annual_cost: UNDETERMINED raise_message "Perform a manual check to verify whether Table G3.1#5a exception #2 is applicable, and it can be demonstrated that the building orientation is dictated by site considerations."` **Notes/Questions:** -None +1. BBUEC, BBREC should probably also be the averages of all present baseline RMDs sums of regulated and unregulated end use costs, however we cannot determine end use costs from the schema +2. the language in 90.1 only includes the defined term "baseline building performance" in the direction to average results from different orientations, it does not include BBUEC and BBREC so maybe #1 is not an issue **[Back](_toc.md)** From 9a88b9fb685a853148be188407868fb09c35d073 Mon Sep 17 00:00:00 2001 From: Jackson Jarboe <122476654+JacksonJ-KC@users.noreply.github.com> Date: Tue, 14 Oct 2025 21:23:16 -0400 Subject: [PATCH 2/8] ensure PASS case checks number of rotations included --- docs/section5/Rule5-1.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/section5/Rule5-1.md b/docs/section5/Rule5-1.md index 2469bb3a06..ee6eeec187 100644 --- a/docs/section5/Rule5-1.md +++ b/docs/section5/Rule5-1.md @@ -62,10 +62,10 @@ Check the given BBP and calculate the expected BBP based on the output source re - **Rule Assertion:** -- Case 1: Baseline rotations are expected based on the fenestration areas by azimuth and the baseline building performance matches the average of the total source results annual cost from the 4 baseline RMDs: PASS: `if rotation_expected_b and bbp == avg_baseline_total_annual_cost: PASS` +- Case 1: Baseline rotations are expected based on the fenestration areas by azimuth and the baseline building performance matches the average of the total source results annual cost from the 4 baseline RMDs: PASS: `if rotation_expected_b and bbp == avg_baseline_total_annual_cost and len(baseline_rotation_total_annual_costs) == 3: PASS` - Case 2: Baseline rotations are not expected based on the fenestration areas by azimuth and the baseline building performance matches the total source results annual cost from the BASELINE_0 RMD: PASS: `elif !rotation_expected_b and bbp == baseline_0_total_annual_cost: PASS` - Case 3: Baseline rotations are not expected based on the fenestration areas by azimuth and the baseline building performance does not match the total source results annual cost from the BASELINE_0 RMD: FAIL: `elif !rotation_expected_b and bbp != baseline_0_total_annual_cost: FAIL` -- Case 4: Baseline rotations are expected based on the fenestration areas by azimuth and the baseline building performance does not match either the average of the total source results annual cost from the 4 baseline RMDs or the BASELINE_0 RMD: FAIL: `elif rotation_expected_b and bbp != avg_baseline_total_annual_cost and bbp != baseline_0_total_annual_cost: FAIL` +- Case 4: Baseline rotations are expected based on the fenestration areas by azimuth and the baseline building performance does not match either the average of the total source results annual cost from the baseline RMDs or the BASELINE_0 RMD: FAIL: `elif rotation_expected_b and bbp != avg_baseline_total_annual_cost and bbp != baseline_0_total_annual_cost: FAIL` - Case 5: Baseline rotations are expected based on the fenestration areas by azimuth and the baseline building performance matches the total source results annual cost from the BASELINE_0 RMD: UNDETERMINED "Perform a manual check to verify whether Table G3.1#5a exception #2 is applicable, and it can be demonstrated that the building orientation is dictated by site considerations.": `elif rotation_expected_b and bbp == baseline_0_total_annual_cost: UNDETERMINED raise_message "Perform a manual check to verify whether Table G3.1#5a exception #2 is applicable, and it can be demonstrated that the building orientation is dictated by site considerations."` From a0e6f91a17711516c417ceb99f6a7eb031f5dd9b Mon Sep 17 00:00:00 2001 From: Jackson Jarboe <122476654+JacksonJ-KC@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:45:24 -0500 Subject: [PATCH 3/8] update 5-1 logic --- .../ashrae9012019/section5/section5rule1.py | 239 ++++++++++-------- 1 file changed, 127 insertions(+), 112 deletions(-) diff --git a/rct229/rulesets/ashrae9012019/section5/section5rule1.py b/rct229/rulesets/ashrae9012019/section5/section5rule1.py index 42853c7bbb..680c02dd00 100644 --- a/rct229/rulesets/ashrae9012019/section5/section5rule1.py +++ b/rct229/rulesets/ashrae9012019/section5/section5rule1.py @@ -1,4 +1,3 @@ -from pydash import compact 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 @@ -11,6 +10,7 @@ from rct229.rulesets.ashrae9012019.ruleset_functions.get_opaque_surface_type import ( get_opaque_surface_type, ) +from rct229.utils.assertions import assert_ from rct229.schema.schema_enums import SchemaEnums from rct229.utils.jsonpath_utils import find_all, find_one from rct229.utils.pint_utils import ZERO @@ -39,7 +39,7 @@ def __init__(self): each_rule=PRM9012019Rule77j30.RMDRule(), index_rmd=BASELINE_0, id="5-1", - description="There are four baseline rotations (i.e., four baseline models differing in azimuth by 90 degrees and four sets of baseline model results) if vertical fenestration area per each orientation differ by more than 5%.", + description="The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. Exceptions: 1. If it can be demonstrated to the satisfaction of the rat-ing authority that the building orientation is dictated bysite considerations. 2. Buildings where the vertical fenestration area on eachorientation varies by less than 5%.", ruleset_section_title="Envelope", standard_section="Table G3.1#5a baseline column", is_primary_rule=True, @@ -73,56 +73,37 @@ def create_data(self, context, data): rmd_p = context.PROPOSED rmd_u = context.USER - has_baseline_0 = rmd_b0 is not None - has_baseline_90 = rmd_b90 is not None - has_baseline_180 = rmd_b180 is not None - has_baseline_270 = rmd_b270 is not None - has_proposed = rmd_p is not None - has_user = rmd_u is not None + bbp_values = set() - has_baseline_0_output = has_baseline_0 and bool( - find_one("$.model_output", rmd_b0) - ) - has_baseline_90_output = has_baseline_90 and bool( - find_one("$.model_output", rmd_b90) - ) - has_baseline_180_output = has_baseline_180 and bool( - find_one("$.model_output", rmd_b180) - ) - has_baseline_270_output = has_baseline_270 and bool( - find_one("$.model_output", rmd_b270) - ) - has_proposed_output = has_proposed and bool( - find_one("$.model_output", rmd_p) - ) - has_user_output = has_user and bool(find_one("$.model_output", rmd_u)) - - rmd_list = [rmd_b0, rmd_b90, rmd_b180, rmd_b270, rmd_p, rmd_u] - rmd_list_no_user = [rmd_b0, rmd_b90, rmd_b180, rmd_b270, rmd_p] + for rmd in [rmd_u, rmd_b0, rmd_b90, rmd_b180, rmd_b270, rmd_p]: + if rmd is not None: + bbp = find_one( + "$.model_output.baseline_building_performance_energy_cost", rmd + ) + if bbp is not None: + bbp_values.add(bbp) - # count the rmds that aren't None - no_of_rmds = len(compact(rmd_list)) + baseline_0_total_annual_cost = None + if rmd_b0 is not None: + baseline_0_total_annual_cost = sum( + find_all("$.model_output.source_results[*].annual_cost", rmd_b0) + ) - # filter out rmds that aren't None then count the length - no_of_output_instance = len( - compact([find_one("$.model_output", rmd) for rmd in rmd_list_no_user]) - ) + baseline_rotation_total_annual_costs = [] + for rmd in [rmd_b90, rmd_b180, rmd_b270]: + if rmd is not None: + baseline_rotation_total_annual_costs.append( + sum( + find_all( + "$.model_output.source_results[*].annual_cost", rmd + ) + ) + ) return { - "has_baseline_0": has_baseline_0, - "has_baseline_90": has_baseline_90, - "has_baseline_180": has_baseline_180, - "has_baseline_270": has_baseline_270, - "has_proposed": has_proposed, - "has_user": has_user, - "has_baseline_0_output": has_baseline_0_output, - "has_baseline_90_output": has_baseline_90_output, - "has_baseline_180_output": has_baseline_180_output, - "has_baseline_270_output": has_baseline_270_output, - "has_proposed_output": has_proposed_output, - "has_user_output": has_user_output, - "no_of_rmds": no_of_rmds, - "no_of_output_instance": no_of_output_instance, + "bbp_values": bbp_values, + "baseline_0_total_annual_cost": baseline_0_total_annual_cost, + "baseline_rotation_total_annual_costs": baseline_rotation_total_annual_costs, } class BuildingRule(RuleDefinitionBase): @@ -140,27 +121,12 @@ def __init__(self): "unit": "", } }, - fail_msg="Fail unless Table G3.1#5a exception #2 is applicable and it can be demonstrated that the building orientation is dictated by site considerations.", + manual_check_required_msg="Perform a manual check to verify whether Table G3.1#5a exception #2 is applicable, and it can be demonstrated that the building orientation is dictated by site considerations.", ) def get_calc_vals(self, context, data=None): building_b0 = context.BASELINE_0 - has_baseline_0 = data["has_baseline_0"] - has_baseline_90 = data["has_baseline_90"] - has_baseline_180 = data["has_baseline_180"] - has_baseline_270 = data["has_baseline_270"] - has_proposed = data["has_proposed"] - has_user = data["has_user"] - has_baseline_0_output = data["has_baseline_0_output"] - has_baseline_90_output = data["has_baseline_90_output"] - has_baseline_180_output = data["has_baseline_180_output"] - has_baseline_270_output = data["has_baseline_270_output"] - has_proposed_output = data["has_proposed_output"] - has_user_output = data["has_user_output"] - no_of_rmds = data["no_of_rmds"] - no_of_output_instance = data["no_of_output_instance"] - # define a function to get the azimuth's corresponding key def get_key_for_azi(azi): azi_value = azi.to("degrees").m @@ -226,60 +192,109 @@ def get_key_for_azi(azi): ) ) + bbp_values = data["bbp_values"] + baseline_0_total_annual_cost = data["baseline_0_total_annual_cost"] + baseline_rotation_total_annual_costs = data[ + "baseline_rotation_total_annual_costs" + ] + + avg_baseline_total_annual_cost = ( + ( + baseline_0_total_annual_cost + + sum(baseline_rotation_total_annual_costs) + ) + / (len(baseline_rotation_total_annual_costs) + 1) + if baseline_0_total_annual_cost + else None + ) + return { - "no_of_rmds": no_of_rmds, - "no_of_output_instance": no_of_output_instance, "rotation_expected_b": rotation_expected_b, - "has_baseline_0": has_baseline_0, - "has_baseline_90": has_baseline_90, - "has_baseline_180": has_baseline_180, - "has_baseline_270": has_baseline_270, - "has_proposed": has_proposed, - "has_user": has_user, - "has_baseline_0_output": has_baseline_0_output, - "has_baseline_90_output": has_baseline_90_output, - "has_baseline_180_output": has_baseline_180_output, - "has_baseline_270_output": has_baseline_270_output, - "has_proposed_output": has_proposed_output, - "has_user_output": has_user_output, + "bbp_values": bbp_values, + "baseline_0_total_annual_cost": baseline_0_total_annual_cost, + "baseline_rotation_total_annual_costs": baseline_rotation_total_annual_costs, + "avg_baseline_total_annual_cost": avg_baseline_total_annual_cost, } + def manual_check_required(self, context, calc_vals=None, data=None): + rotation_expected_b = calc_vals["rotation_expected_b"] + bbp_values = calc_vals["bbp_values"] + baseline_0_total_annual_cost = calc_vals["baseline_0_total_annual_cost"] + # --- BBP uniqueness check --- + assert_( + len(bbp_values) == 1, + "Ruleset expects exactly one BBP value to be used in the project.", + ) + + bbp = next(iter(bbp_values)) + return rotation_expected_b and bbp == baseline_0_total_annual_cost + def rule_check(self, context, calc_vals=None, data=None): rotation_expected_b = calc_vals["rotation_expected_b"] - no_of_rmds = calc_vals["no_of_rmds"] - no_of_output_instance = calc_vals["no_of_output_instance"] - - has_baseline_0 = calc_vals["has_baseline_0"] - has_baseline_90 = calc_vals["has_baseline_90"] - has_baseline_180 = calc_vals["has_baseline_180"] - has_baseline_270 = calc_vals["has_baseline_270"] - has_proposed = calc_vals["has_proposed"] - has_user = calc_vals["has_user"] - - has_baseline_0_output = calc_vals["has_baseline_0_output"] - has_baseline_90_output = calc_vals["has_baseline_90_output"] - has_baseline_180_output = calc_vals["has_baseline_180_output"] - has_baseline_270_output = calc_vals["has_baseline_270_output"] - has_proposed_output = calc_vals["has_proposed_output"] - - return ( - has_user - and has_proposed - and has_baseline_0 - and has_proposed_output - and has_baseline_0_output - ) and ( - ( - rotation_expected_b - and has_baseline_90 - and has_baseline_180 - and has_baseline_270 - and has_baseline_90_output - and has_baseline_180_output - and has_baseline_270_output - and no_of_rmds == 6 - and no_of_output_instance == 5 - ) - or (not rotation_expected_b) + bbp_values = calc_vals["bbp_values"] + baseline_0_total_annual_cost = calc_vals["baseline_0_total_annual_cost"] + baseline_rotation_total_annual_costs = calc_vals[ + "baseline_rotation_total_annual_costs" + ] + avg_baseline_total_annual_cost = calc_vals[ + "avg_baseline_total_annual_cost" + ] + + # --- BBP uniqueness check --- + assert_( + len(bbp_values) == 1, + "Ruleset expects exactly one BBP value to be used in the project.", ) + + bbp = next(iter(bbp_values)) + + # ---------- CASES ---------- + + # Case 1: + # rotations expected AND BBP == avg of 4 baseline annual costs + if ( + rotation_expected_b + and len(baseline_rotation_total_annual_costs) == 3 + and bbp == avg_baseline_total_annual_cost + ): + return True + + # Case 2: + # rotations NOT expected AND BBP == baseline_0 annual cost + if not rotation_expected_b and bbp == baseline_0_total_annual_cost: + return True + + # Case 3: + # rotations NOT expected AND BBP != baseline_0 annual cost + if not rotation_expected_b and bbp != baseline_0_total_annual_cost: + return False + + # Case 4: + # rotations expected AND BBP matches neither avg nor baseline_0 + else: + return False + + def get_fail_msg(self, context, calc_vals=None, data=None): + rotation_expected_b = calc_vals["rotation_expected_b"] + + bbp_values = calc_vals["bbp_values"] + baseline_0_total_annual_cost = calc_vals["baseline_0_total_annual_cost"] + baseline_rotation_total_annual_costs = calc_vals[ + "baseline_rotation_total_annual_costs" + ] + avg_baseline_total_annual_cost = calc_vals[ + "avg_baseline_total_annual_cost" + ] + bbp = next(iter(bbp_values)) + if ( + rotation_expected_b + and len(baseline_rotation_total_annual_costs) < 3 + ): + return "Baseline building performance could not be validated because one or more of the required baseline rotation simulations (90, 180, or 270 degrees) did not include Source Result annual cost." + elif rotation_expected_b and bbp != avg_baseline_total_annual_cost: + return f"Baseline building performance energy cost ({bbp}) does not equal the average of the four baseline orientation annual costs ({avg_baseline_total_annual_cost})." + elif not rotation_expected_b and bbp != baseline_0_total_annual_cost: + return f"Baseline building performance energy cost ({bbp}) does not equal the baseline (0 degree) annual cost ({baseline_0_total_annual_cost})." + else: + return "Baseline building performance could not be validated." From 1075ffc90a1a836327b2ddc03622a1426f907ba0 Mon Sep 17 00:00:00 2001 From: Jackson Jarboe <122476654+JacksonJ-KC@users.noreply.github.com> Date: Wed, 24 Dec 2025 09:54:08 -0500 Subject: [PATCH 4/8] simplify bbp assertion --- .../ashrae9012019/section5/section5rule1.py | 66 ++++++++++--------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/rct229/rulesets/ashrae9012019/section5/section5rule1.py b/rct229/rulesets/ashrae9012019/section5/section5rule1.py index 680c02dd00..af1475ba5b 100644 --- a/rct229/rulesets/ashrae9012019/section5/section5rule1.py +++ b/rct229/rulesets/ashrae9012019/section5/section5rule1.py @@ -39,7 +39,10 @@ def __init__(self): each_rule=PRM9012019Rule77j30.RMDRule(), index_rmd=BASELINE_0, id="5-1", - description="The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. Exceptions: 1. If it can be demonstrated to the satisfaction of the rat-ing authority that the building orientation is dictated bysite considerations. 2. Buildings where the vertical fenestration area on eachorientation varies by less than 5%.", + description="The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. " + "Exceptions: " + "1. If it can be demonstrated to the satisfaction of the rating authority that the building orientation is dictated bysite considerations. " + "2. Buildings where the vertical fenestration area on each orientation varies by less than 5%.", ruleset_section_title="Envelope", standard_section="Table G3.1#5a baseline column", is_primary_rule=True, @@ -100,8 +103,14 @@ def create_data(self, context, data): ) ) + # --- BBP uniqueness check --- + assert_( + len(bbp_values) == 1, + "Ruleset expects exactly one BBP value to be used in the project.", + ) + return { - "bbp_values": bbp_values, + "bbp_value": next(iter(bbp_values)), "baseline_0_total_annual_cost": baseline_0_total_annual_cost, "baseline_rotation_total_annual_costs": baseline_rotation_total_annual_costs, } @@ -192,7 +201,6 @@ def get_key_for_azi(azi): ) ) - bbp_values = data["bbp_values"] baseline_0_total_annual_cost = data["baseline_0_total_annual_cost"] baseline_rotation_total_annual_costs = data[ "baseline_rotation_total_annual_costs" @@ -210,7 +218,7 @@ def get_key_for_azi(azi): return { "rotation_expected_b": rotation_expected_b, - "bbp_values": bbp_values, + "bbp_value": data["bbp_value"], "baseline_0_total_annual_cost": baseline_0_total_annual_cost, "baseline_rotation_total_annual_costs": baseline_rotation_total_annual_costs, "avg_baseline_total_annual_cost": avg_baseline_total_annual_cost, @@ -218,21 +226,15 @@ def get_key_for_azi(azi): def manual_check_required(self, context, calc_vals=None, data=None): rotation_expected_b = calc_vals["rotation_expected_b"] - bbp_values = calc_vals["bbp_values"] + bbp_value = calc_vals["bbp_value"] baseline_0_total_annual_cost = calc_vals["baseline_0_total_annual_cost"] - # --- BBP uniqueness check --- - assert_( - len(bbp_values) == 1, - "Ruleset expects exactly one BBP value to be used in the project.", - ) - bbp = next(iter(bbp_values)) - return rotation_expected_b and bbp == baseline_0_total_annual_cost + return rotation_expected_b and bbp_value == baseline_0_total_annual_cost def rule_check(self, context, calc_vals=None, data=None): rotation_expected_b = calc_vals["rotation_expected_b"] - bbp_values = calc_vals["bbp_values"] + bbp_value = calc_vals["bbp_value"] baseline_0_total_annual_cost = calc_vals["baseline_0_total_annual_cost"] baseline_rotation_total_annual_costs = calc_vals[ "baseline_rotation_total_annual_costs" @@ -241,33 +243,30 @@ def rule_check(self, context, calc_vals=None, data=None): "avg_baseline_total_annual_cost" ] - # --- BBP uniqueness check --- - assert_( - len(bbp_values) == 1, - "Ruleset expects exactly one BBP value to be used in the project.", - ) - - bbp = next(iter(bbp_values)) - # ---------- CASES ---------- - # Case 1: # rotations expected AND BBP == avg of 4 baseline annual costs if ( rotation_expected_b and len(baseline_rotation_total_annual_costs) == 3 - and bbp == avg_baseline_total_annual_cost + and bbp_value == avg_baseline_total_annual_cost ): return True # Case 2: # rotations NOT expected AND BBP == baseline_0 annual cost - if not rotation_expected_b and bbp == baseline_0_total_annual_cost: + if ( + not rotation_expected_b + and bbp_value == baseline_0_total_annual_cost + ): return True # Case 3: # rotations NOT expected AND BBP != baseline_0 annual cost - if not rotation_expected_b and bbp != baseline_0_total_annual_cost: + if ( + not rotation_expected_b + and bbp_value != baseline_0_total_annual_cost + ): return False # Case 4: @@ -278,7 +277,7 @@ def rule_check(self, context, calc_vals=None, data=None): def get_fail_msg(self, context, calc_vals=None, data=None): rotation_expected_b = calc_vals["rotation_expected_b"] - bbp_values = calc_vals["bbp_values"] + bbp_value = calc_vals["bbp_value"] baseline_0_total_annual_cost = calc_vals["baseline_0_total_annual_cost"] baseline_rotation_total_annual_costs = calc_vals[ "baseline_rotation_total_annual_costs" @@ -286,15 +285,20 @@ def get_fail_msg(self, context, calc_vals=None, data=None): avg_baseline_total_annual_cost = calc_vals[ "avg_baseline_total_annual_cost" ] - bbp = next(iter(bbp_values)) + if ( rotation_expected_b and len(baseline_rotation_total_annual_costs) < 3 ): return "Baseline building performance could not be validated because one or more of the required baseline rotation simulations (90, 180, or 270 degrees) did not include Source Result annual cost." - elif rotation_expected_b and bbp != avg_baseline_total_annual_cost: - return f"Baseline building performance energy cost ({bbp}) does not equal the average of the four baseline orientation annual costs ({avg_baseline_total_annual_cost})." - elif not rotation_expected_b and bbp != baseline_0_total_annual_cost: - return f"Baseline building performance energy cost ({bbp}) does not equal the baseline (0 degree) annual cost ({baseline_0_total_annual_cost})." + elif ( + rotation_expected_b and bbp_value != avg_baseline_total_annual_cost + ): + return f"Baseline building performance energy cost ({bbp_value}) does not equal the average of the four baseline orientation annual costs ({avg_baseline_total_annual_cost})." + elif ( + not rotation_expected_b + and bbp_value != baseline_0_total_annual_cost + ): + return f"Baseline building performance energy cost ({bbp_value}) does not equal the baseline (0 degree) annual cost ({baseline_0_total_annual_cost})." else: return "Baseline building performance could not be validated." From d9780b205818059b7b0ec168af2279054613c8b7 Mon Sep 17 00:00:00 2001 From: jugonzal07 Date: Mon, 16 Feb 2026 18:57:07 -0500 Subject: [PATCH 5/8] Updated 5-1 test JSON to match the updated test logic for Rule 5-1 found in RDS. --- .../ashrae9012019/ENV/rule_5_1.json | 809 +++++++++++++++++- 1 file changed, 790 insertions(+), 19 deletions(-) diff --git a/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json b/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json index e46516ef9f..a503b38e45 100644 --- a/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json +++ b/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json @@ -3,12 +3,12 @@ "Section": 5, "Rule": 1, "Test": "a", - "test_description": "Project has one building segment with two above-grade wall surfaces at different azimuth angles. Each wall has a window and the windows differ in area by more than 5%. Four sets of baseline model results are appropriately provided.", + "test_description": "Project has one building segment with windows that differ by >5% (rotations expected), four baseline rotations provided, BBP equals the average of all four rotation costs.", "expected_rule_outcome": "pass", "standard": { "rule_id": "5-1", "ruleset_reference": "Table G3.1(5) Baseline Building Performance (a)", - "rule_description": "There are four baseline rotations (i.e., four baseline models differing in azimuth by 90 degrees and four sets of baseline model results) if vertical fenestration area per each orientation differ by more than 5%.", + "rule_description": "The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. Exceptions: 1. If it can be demonstrated to the satisfaction of the rating authority that the building orientation is dictated by site considerations. 2. Buildings where the vertical fenestration area on each orientation varies by less than 5%", "applicable_rmr": "Baseline Model", "rule_assertion": "=", "comparison_value": "Expected Value", @@ -28,7 +28,8 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 50000 }, "buildings": [ { @@ -94,7 +95,8 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 50000 }, "buildings": [ { @@ -160,7 +162,20 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 50000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 30000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 18000 + } + ] }, "buildings": [ { @@ -221,7 +236,20 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 50000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 31000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 20000 + } + ] }, "buildings": [ { @@ -282,7 +310,20 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 50000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 29000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 19000 + } + ] }, "buildings": [ { @@ -343,7 +384,20 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 50000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 32000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 21000 + } + ] }, "buildings": [ { @@ -403,12 +457,12 @@ "Section": 5, "Rule": 1, "Test": "b", - "test_description": "Project has one building segment with two above-grade wall surfaces at different azimuth angles. Each wall has a window and the windows differ in area by less than 5%. One set of baseline model results is appropriately provided.", + "test_description": "Project has one building segment with two above-grade wall surfaces that differ by <5% (rotations NOT expected), one baseline provided, BBP equals the single baseline cost.", "expected_rule_outcome": "pass", "standard": { "rule_id": "5-1", "ruleset_reference": "Table G3.1(5) Baseline Building Performance (a)", - "rule_description": "There are four baseline rotations (i.e., four baseline models differing in azimuth by 90 degrees and four sets of baseline model results) if vertical fenestration area per each orientation differ by more than 5%.", + "rule_description": "The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. Exceptions: 1. If it can be demonstrated to the satisfaction of the rating authority that the building orientation is dictated by site considerations. 2. Buildings where the vertical fenestration area on each orientation varies by less than 5%", "applicable_rmr": "Baseline Model", "rule_assertion": "=", "comparison_value": "Expected Value", @@ -428,7 +482,8 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 45000 }, "buildings": [ { @@ -494,7 +549,8 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 45000 }, "buildings": [ { @@ -560,7 +616,20 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 45000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 28000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 17000 + } + ] }, "buildings": [ { @@ -620,12 +689,12 @@ "Section": 5, "Rule": 1, "Test": "c", - "test_description": "Project has one building segment with two above-grade wall surfaces at different azimuth angles. Each wall has a window and the windows differ in area by more than 5%. One set of baseline model results is provided, when four sets are expected.", + "test_description": "Project has one building segment with two above-grade wall surfaces with windows that differ by <5% (rotations NOT expected), one baseline provided, but BBP does NOT match the baseline cost.", "expected_rule_outcome": "fail", "standard": { "rule_id": "5-1", "ruleset_reference": "Table G3.1(5) Baseline Building Performance (a)", - "rule_description": "There are four baseline rotations (i.e., four baseline models differing in azimuth by 90 degrees and four sets of baseline model results) if vertical fenestration area per each orientation differ by more than 5%.", + "rule_description": "The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. Exceptions: 1. If it can be demonstrated to the satisfaction of the rating authority that the building orientation is dictated by site considerations. 2. Buildings where the vertical fenestration area on each orientation varies by less than 5%", "applicable_rmr": "Baseline Model", "rule_assertion": "=", "comparison_value": "Expected Value", @@ -645,7 +714,695 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 99999 + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 20, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 2.22967296 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 190, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "USER" + } + ] + }, + "proposed": { + "id": "ASHRAE229", + "ruleset_model_descriptions": [ + { + "id": "RMD 1", + "model_output": { + "id": "Output Instance 1", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 99999 + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 20, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 2.22967296 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 190, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "PROPOSED" + } + ] + }, + "baseline": { + "id": "ASHRAE229", + "ruleset_model_descriptions": [ + { + "id": "RMD 1", + "model_output": { + "id": "Output Instance 1", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 99999, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 28000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 17000 + } + ] + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 20, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 2.22967296 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 190, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "BASELINE_0" + } + ] + } + } + }, + "rule-5-1-d": { + "Section": 5, + "Rule": 1, + "Test": "d", + "test_description": "Project has one building segment with two above-grade wall surfaces with windows that differ by >5% (rotations expected), four baselines provided, but BBP does NOT match the average (matches only one rotation).", + "expected_rule_outcome": "fail", + "standard": { + "rule_id": "5-1", + "ruleset_reference": "Table G3.1(5) Baseline Building Performance (a)", + "rule_description": "The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. Exceptions: 1. If it can be demonstrated to the satisfaction of the rating authority that the building orientation is dictated by site considerations. 2. Buildings where the vertical fenestration area on each orientation varies by less than 5%", + "applicable_rmr": "Baseline Model", + "rule_assertion": "=", + "comparison_value": "Expected Value", + "primary_rule": "Full", + "schema_version": "0.0.36" + }, + "rmd_transformations": { + "user": { + "id": "ASHRAE229", + "ruleset_model_descriptions": [ + { + "id": "RMD 1", + "model_output": { + "id": "Output Instance 1", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000 + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 20, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 1.3935456 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 190, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "USER" + } + ] + }, + "proposed": { + "id": "ASHRAE229", + "ruleset_model_descriptions": [ + { + "id": "RMD 1", + "model_output": { + "id": "Output Instance 1", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000 + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 20, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 1.3935456 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 190, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "PROPOSED" + } + ] + }, + "baseline": { + "id": "ASHRAE229", + "ruleset_model_descriptions": [ + { + "id": "RMD 1", + "model_output": { + "id": "Output Instance 1", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 30000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 18000 + } + ] + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 20, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 1.3935456 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 190, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "BASELINE_0" + }, + { + "id": "RMD 2", + "model_output": { + "id": "Output Instance 2", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 31000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 20000 + } + ] + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 110, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 1.3935456 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 280, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "BASELINE_90" + }, + { + "id": "RMD 3", + "model_output": { + "id": "Output Instance 3", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 29000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 19000 + } + ] + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 200, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 1.3935456 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 10, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "BASELINE_180" + }, + { + "id": "RMD 4", + "model_output": { + "id": "Output Instance 4", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 32000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 21000 + } + ] + }, + "buildings": [ + { + "id": "Building 1", + "building_open_schedule": "Required Schedule 1", + "building_segments": [ + { + "id": "Building Segment 1", + "zones": [ + { + "id": "Zone 1", + "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", + "thermostat_heating_setpoint_schedule": "Heating Schedule 1", + "surfaces": [ + { + "id": "Surface 1", + "azimuth": 290, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 1", + "classification": "WINDOW", + "glazed_area": 1.3935456 + } + ] + }, + { + "id": "Surface 2", + "azimuth": 100, + "adjacent_to": "EXTERIOR", + "tilt": 90, + "area": 92.90303999999999, + "subsurfaces": [ + { + "id": "Window 2", + "classification": "WINDOW", + "glazed_area": 2.3225759999999998 + } + ] + } + ] + } + ] + } + ] + } + ], + "type": "BASELINE_270" + } + ] + } + } + }, + "rule-5-1-e": { + "Section": 5, + "Rule": 1, + "Test": "e", + "test_description": "Project has one building segment with two above-grade wall surfaces with windows that differ by >5% (rotations expected), but only one baseline provided with BBP matching that single baseline. Requires manual check for site exception.", + "expected_rule_outcome": "undetermined", + "expected_raised_message_includes": "Perform a manual check to verify whether Table G3.1#5a exception #2 is applicable, and it can be demonstrated that the building orientation is dictated by site considerations.", + "standard": { + "rule_id": "5-1", + "ruleset_reference": "Table G3.1(5) Baseline Building Performance (a)", + "rule_description": "The baseline building performance shall be generated by simulating the building with its actual orientation and again after rotating the entire building 90, 180, and 270 degrees, then averaging the results. Exceptions: 1. If it can be demonstrated to the satisfaction of the rating authority that the building orientation is dictated by site considerations. 2. Buildings where the vertical fenestration area on each orientation varies by less than 5%", + "applicable_rmr": "Baseline Model", + "rule_assertion": "=", + "comparison_value": "Expected Value", + "primary_rule": "Full", + "schema_version": "0.0.29" + }, + "rmd_transformations": { + "user": { + "id": "ASHRAE229", + "ruleset_model_descriptions": [ + { + "id": "RMD 1", + "model_output": { + "id": "Output Instance 1", + "unmet_load_hours": 0, + "unmet_load_hours_heating": 0, + "unmet_load_hours_cooling": 0, + "unmet_occupied_load_hours_heating": 0, + "unmet_occupied_load_hours_cooling": 0, + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000 }, "buildings": [ { @@ -711,7 +1468,8 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000 }, "buildings": [ { @@ -777,7 +1535,20 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000 + "building_peak_cooling_load": 25000, + "baseline_building_performance_energy_cost": 48000, + "annual_source_results": [ + { + "id": "Source Result 1", + "energy_source": "ELECTRICITY", + "annual_cost": 30000 + }, + { + "id": "Source Result 2", + "energy_source": "NATURAL_GAS", + "annual_cost": 18000 + } + ] }, "buildings": [ { @@ -833,4 +1604,4 @@ } } } -} \ No newline at end of file +} From 5dd52b667155939635803b5f23e750de3f98e9cf Mon Sep 17 00:00:00 2001 From: jugonzal07 Date: Fri, 27 Feb 2026 18:44:00 -0500 Subject: [PATCH 6/8] Fixed issues with 5-1 rule test schema. Removed redundant schedules as well. --- .../ashrae9012019/ENV/rule_5_1.json | 166 ++++++++---------- 1 file changed, 71 insertions(+), 95 deletions(-) diff --git a/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json b/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json index a503b38e45..8b828a4ab6 100644 --- a/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json +++ b/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json @@ -18,6 +18,10 @@ "rmd_transformations": { "user": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 50000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -28,21 +32,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 50000 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -85,6 +85,10 @@ }, "proposed": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 50000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -95,21 +99,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 50000 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -152,6 +152,10 @@ }, "baseline": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 50000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -163,7 +167,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 50000, "annual_source_results": [ { "id": "Source Result 1", @@ -180,15 +183,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -237,7 +237,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 50000, "annual_source_results": [ { "id": "Source Result 1", @@ -254,15 +253,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -311,7 +307,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 50000, "annual_source_results": [ { "id": "Source Result 1", @@ -328,15 +323,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -385,7 +377,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 50000, "annual_source_results": [ { "id": "Source Result 1", @@ -402,15 +393,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -472,6 +460,10 @@ "rmd_transformations": { "user": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 45000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -482,21 +474,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 45000 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -539,6 +527,10 @@ }, "proposed": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 45000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -549,21 +541,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 45000 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -606,6 +594,10 @@ }, "baseline": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 45000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -617,7 +609,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 45000, "annual_source_results": [ { "id": "Source Result 1", @@ -634,15 +625,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -704,6 +692,10 @@ "rmd_transformations": { "user": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 99999 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -714,21 +706,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 99999 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -771,6 +759,10 @@ }, "proposed": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 99999 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -781,21 +773,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 99999 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -838,6 +826,10 @@ }, "baseline": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 99999 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -849,7 +841,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 99999, "annual_source_results": [ { "id": "Source Result 1", @@ -866,15 +857,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -936,6 +924,10 @@ "rmd_transformations": { "user": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 48000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -946,21 +938,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1003,6 +991,10 @@ }, "proposed": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 48000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -1013,21 +1005,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1070,6 +1058,10 @@ }, "baseline": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 48000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -1081,7 +1073,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000, "annual_source_results": [ { "id": "Source Result 1", @@ -1098,15 +1089,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1155,7 +1143,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000, "annual_source_results": [ { "id": "Source Result 1", @@ -1172,15 +1159,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1229,7 +1213,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000, "annual_source_results": [ { "id": "Source Result 1", @@ -1246,15 +1229,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1303,7 +1283,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000, "annual_source_results": [ { "id": "Source Result 1", @@ -1320,15 +1299,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1391,6 +1367,10 @@ "rmd_transformations": { "user": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 48000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -1401,21 +1381,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1458,6 +1434,10 @@ }, "proposed": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 48000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -1468,21 +1448,17 @@ "unmet_load_hours_cooling": 0, "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, - "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000 + "building_peak_cooling_load": 25000 }, "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1525,6 +1501,10 @@ }, "baseline": { "id": "ASHRAE229", + "output": { + "id": "Output 1", + "baseline_building_performance_energy_cost": 48000 + }, "ruleset_model_descriptions": [ { "id": "RMD 1", @@ -1536,7 +1516,6 @@ "unmet_occupied_load_hours_heating": 0, "unmet_occupied_load_hours_cooling": 0, "building_peak_cooling_load": 25000, - "baseline_building_performance_energy_cost": 48000, "annual_source_results": [ { "id": "Source Result 1", @@ -1553,15 +1532,12 @@ "buildings": [ { "id": "Building 1", - "building_open_schedule": "Required Schedule 1", "building_segments": [ { "id": "Building Segment 1", "zones": [ { "id": "Zone 1", - "thermostat_cooling_setpoint_schedule": "Cooling Schedule 1", - "thermostat_heating_setpoint_schedule": "Heating Schedule 1", "surfaces": [ { "id": "Surface 1", @@ -1604,4 +1580,4 @@ } } } -} +} \ No newline at end of file From 1015545c6a7b812318c42312a647b3a740f6a469 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Fri, 27 Feb 2026 17:21:03 -0800 Subject: [PATCH 7/8] Move the bbp level --- .../ashrae9012019/section5/section5rule1.py | 57 +++++++++++-------- 1 file changed, 32 insertions(+), 25 deletions(-) diff --git a/rct229/rulesets/ashrae9012019/section5/section5rule1.py b/rct229/rulesets/ashrae9012019/section5/section5rule1.py index af1475ba5b..6effe8ff2d 100644 --- a/rct229/rulesets/ashrae9012019/section5/section5rule1.py +++ b/rct229/rulesets/ashrae9012019/section5/section5rule1.py @@ -1,17 +1,15 @@ 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 ( - BASELINE_0, -) +from rct229.rulesets.ashrae9012019 import BASELINE_0 from rct229.rulesets.ashrae9012019.ruleset_functions.get_opaque_surface_type import ( OpaqueSurfaceType as OST, ) from rct229.rulesets.ashrae9012019.ruleset_functions.get_opaque_surface_type import ( get_opaque_surface_type, ) -from rct229.utils.assertions import assert_ from rct229.schema.schema_enums import SchemaEnums +from rct229.utils.assertions import assert_ from rct229.utils.jsonpath_utils import find_all, find_one from rct229.utils.pint_utils import ZERO @@ -49,6 +47,31 @@ def __init__(self): list_path="ruleset_model_descriptions[0]", ) + def create_data(self, context, data): + rmd_b0 = context.BASELINE_0 + rmd_b90 = context.BASELINE_90 + rmd_b180 = context.BASELINE_180 + rmd_b270 = context.BASELINE_270 + rmd_p = context.PROPOSED + rmd_u = context.USER + + bbp_values = set() + for rmd in [rmd_u, rmd_b0, rmd_b90, rmd_b180, rmd_b270, rmd_p]: + if rmd is not None: + bbp = find_one( + "$.output.baseline_building_performance_energy_cost", rmd + ) + if bbp is not None: + bbp_values.add(bbp) + + # --- BBP uniqueness check --- + assert_( + len(bbp_values) == 1, + "Ruleset expects exactly one BBP value to be used in the project.", + ) + + return {"bbp_value": next(iter(bbp_values))} + class RMDRule(RuleDefinitionListIndexedBase): def __init__(self): super(PRM9012019Rule77j30.RMDRule, self).__init__( @@ -73,23 +96,13 @@ def create_data(self, context, data): rmd_b90 = context.BASELINE_90 rmd_b180 = context.BASELINE_180 rmd_b270 = context.BASELINE_270 - rmd_p = context.PROPOSED - rmd_u = context.USER - - bbp_values = set() - - for rmd in [rmd_u, rmd_b0, rmd_b90, rmd_b180, rmd_b270, rmd_p]: - if rmd is not None: - bbp = find_one( - "$.model_output.baseline_building_performance_energy_cost", rmd - ) - if bbp is not None: - bbp_values.add(bbp) baseline_0_total_annual_cost = None if rmd_b0 is not None: baseline_0_total_annual_cost = sum( - find_all("$.model_output.source_results[*].annual_cost", rmd_b0) + find_all( + "$.model_output.annual_source_results[*].annual_cost", rmd_b0 + ) ) baseline_rotation_total_annual_costs = [] @@ -98,19 +111,13 @@ def create_data(self, context, data): baseline_rotation_total_annual_costs.append( sum( find_all( - "$.model_output.source_results[*].annual_cost", rmd + "$.model_output.annual_source_results[*].annual_cost", + rmd, ) ) ) - # --- BBP uniqueness check --- - assert_( - len(bbp_values) == 1, - "Ruleset expects exactly one BBP value to be used in the project.", - ) - return { - "bbp_value": next(iter(bbp_values)), "baseline_0_total_annual_cost": baseline_0_total_annual_cost, "baseline_rotation_total_annual_costs": baseline_rotation_total_annual_costs, } From 9c5a9f4554557255cd7505ae2f237c754298d471 Mon Sep 17 00:00:00 2001 From: yunjoonjung Date: Fri, 27 Feb 2026 17:27:27 -0800 Subject: [PATCH 8/8] Updated case (d) baseline annual cost --- .../ruletest_jsons/ashrae9012019/ENV/rule_5_1.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json b/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json index 8b828a4ab6..b52cbb3bbb 100644 --- a/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json +++ b/rct229/ruletest_engine/ruletest_jsons/ashrae9012019/ENV/rule_5_1.json @@ -1077,7 +1077,7 @@ { "id": "Source Result 1", "energy_source": "ELECTRICITY", - "annual_cost": 30000 + "annual_cost": 40000 }, { "id": "Source Result 2",