diff --git a/pyproject.toml b/pyproject.toml index 4ac474251e..cbf97f62ce 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "ruleset-checking-tool" -version = "0.2.6" +version = "0.2.7" description = "PNNL ruleset checking tool" authors = ["Weili Xu ", "Charlie Holly ", "Juan Gonzalez ", "Yun Joon Jung ", "Jiarong Xie "] license = "MIT" diff --git a/rct229/rule_engine/engine.py b/rct229/rule_engine/engine.py index 79f44b3100..3bf1825310 100644 --- a/rct229/rule_engine/engine.py +++ b/rct229/rule_engine/engine.py @@ -27,7 +27,7 @@ def get_available_rules(): return available_rules -def evaluate_all_rules_rpd(ruleset_project_descriptions): +def evaluate_all_rules_rpd(ruleset_project_descriptions, session_id=""): # Get reference to rule functions in rules model available_rule_definitions = rulesets.__getrules__() ruleset_models = get_rmd_instance() @@ -49,7 +49,7 @@ def evaluate_all_rules_rpd(ruleset_project_descriptions): print("Processing rules...") rules_list = [rule_def[1]() for rule_def in available_rule_definitions] - report = evaluate_rules(rules_list, ruleset_models) + report = evaluate_rules(rules_list, ruleset_models, session_id=session_id) report["rpd_files"] = rpd_rmd_map_list return report @@ -135,7 +135,11 @@ def evaluate_rule(rule, rmrs, test=False): def evaluate_rules( - rules_list: list, rmds: RuleSetModels, unit_system=UNIT_SYSTEM.IP, test=False + rules_list: list, + rmds: RuleSetModels, + unit_system=UNIT_SYSTEM.IP, + test=False, + session_id="", ): """Evaluates a list of rules against an RMDs @@ -147,6 +151,7 @@ def evaluate_rules( Object containing RPDs for ruleset evaluation test: Boolean Flag to indicate whether this run is for software testing workflow or not. + session_id: string Returns ------- @@ -227,7 +232,7 @@ def evaluate_rules( rule_counter += 1 if rule_counter in counting_steps: print( - f"Compliance evaluation progress: {round(rule_counter / total_num_rules * 100)}%" + f"Project Evaluation Session ID: #{session_id}# => Compliance evaluation progress: {round(rule_counter / total_num_rules * 100)}%" ) print(f"Processing Rule {rule.id}") outcome = rule.evaluate(copied_rmds) diff --git a/rct229/rulesets/ashrae9012019/section1/section1rule6.py b/rct229/rulesets/ashrae9012019/section1/section1rule6.py index 682353e7a9..572c959523 100644 --- a/rct229/rulesets/ashrae9012019/section1/section1rule6.py +++ b/rct229/rulesets/ashrae9012019/section1/section1rule6.py @@ -14,9 +14,9 @@ def __init__(self): USER=False, BASELINE_0=True, PROPOSED=True ), id="1-6", - description="temp", + description="The proposed design shall be the same as the baseline design for all data elements identified in the schema hosted at data.standards.ashrae {{https://github.com/open229/ruleset-model-description-schema/blob/main/docs229/ASHRAE229_extra.schema.json}}", ruleset_section_title="Performance Calculation", - standard_section="a", + standard_section="Table G3.1(1) Baseline Building Performance (a)", is_primary_rule=True, rmd_context="", ) diff --git a/rct229/rulesets/ashrae9012019/section1/section1rule7.py b/rct229/rulesets/ashrae9012019/section1/section1rule7.py index 0fb876db8c..2764ad73d6 100644 --- a/rct229/rulesets/ashrae9012019/section1/section1rule7.py +++ b/rct229/rulesets/ashrae9012019/section1/section1rule7.py @@ -13,9 +13,9 @@ def __init__(self): USER=True, BASELINE_0=False, PROPOSED=True ), id="1-7", - description="temp", + description="The proposed design shall be the same as the user design for all data elements identified in the schema hosted at data.standards.ashrae {{https://github.com/open229/ruleset-model-description-schema/blob/main/docs229/ASHRAE229_extra.schema.json}}", ruleset_section_title="Performance Calculation", - standard_section="a", + standard_section="Table G3.1(1) Proposed Building Performance (a)", is_primary_rule=True, rmd_context="", ) diff --git a/rct229/utils/std_comparisons.py b/rct229/utils/std_comparisons.py index a5753bde5d..4626fa67dd 100644 --- a/rct229/utils/std_comparisons.py +++ b/rct229/utils/std_comparisons.py @@ -1,3 +1,8 @@ +import operator +from pint import Quantity +from decimal import Decimal, ROUND_HALF_UP + + """ Global tolerance for equality comparison. Default allows 0.5% variations of generated baseline/proposed from the standard specify value. """ @@ -29,3 +34,85 @@ def std_equal( True iff the val is within percent_tolerance of std_val """ return abs(std_val - val) <= (percent_tolerance / 100) * abs(std_val) + + +def std_equal_with_precision( + val: Quantity | float | int, + std_val: Quantity | float | int, + precision: Quantity | float | int, +) -> bool: + """Determines whether the model value and standard value are equal with the specified precision. If any of the function inputs are a Quantity type, then all function inputs must be Quantity. + + Parameters + ---------- + val: Quantity | float | int + value extracted from model + std_val : Quantity | float | int + standard value from code + precision: Quantity | float | int + number of decimal places or significant value to round to, and intended units of the comparison + + Returns + ------- + bool + True if the modeled value is equal to the standard value within the specified precision + """ + # Check if all or none of the arguments are Quantity types + are_quantities = [isinstance(arg, Quantity) for arg in [val, std_val, precision]] + if not (all(are_quantities) or not any(are_quantities)): + raise TypeError( + "Arguments must be consistent in type: all Quantity or all non-Quantity." + ) + + # Determine if the values are pint Quantities and handle accordingly + if ( + isinstance(val, Quantity) + and isinstance(std_val, Quantity) + and isinstance(precision, Quantity) + ): + units = precision.units + val = val.to(units) + std_val = std_val.to(units) + val_magnitude = Decimal(str(val.magnitude)) + std_val_magnitude = Decimal(str(std_val.magnitude)) + precision_magnitude = Decimal(str(precision.magnitude)) + else: + val_magnitude = Decimal(str(val)) + std_val_magnitude = Decimal(str(std_val)) + precision_magnitude = Decimal(str(precision)) + + # Determine rounding precision based on whether precision is a whole number or a decimal + if precision_magnitude.as_tuple().exponent < 0: + # Decimal places (e.g., 0.01) + precision_decimal_places = abs(precision_magnitude.as_tuple().exponent) + rounding_precision = f"1E-{str(precision_decimal_places)}" + else: + # Whole number (e.g., 10, 100) + rounding_precision = f"1E+{str(int(precision_magnitude.log10()))}" + + # Round both values to the specified precision + val_rounded = val_magnitude.quantize( + Decimal(rounding_precision), rounding=ROUND_HALF_UP + ) + std_val_rounded = std_val_magnitude.quantize( + Decimal(rounding_precision), rounding=ROUND_HALF_UP + ) + + # Compare the rounded values + return val_rounded == std_val_rounded + + +def std_conservative_outcome( + val: Quantity, std_val: Quantity, conservative_operator_wrt_std: operator +): + """Determines if the model value has a conservative outcome compared to the standard value. + + Parameters + ---------- + val: Quantity + value extracted from model + std_val : Quantity + standard value from code + conservative_operator_wrt_std: operator that results in a conservative outcome compared to the standard value + """ + return conservative_operator_wrt_std(val, std_val) diff --git a/rct229/utils/std_comparisons_test.py b/rct229/utils/std_comparisons_test.py index 4c1a575e37..7d87069a54 100644 --- a/rct229/utils/std_comparisons_test.py +++ b/rct229/utils/std_comparisons_test.py @@ -1,11 +1,16 @@ import operator +import pytest from rct229.schema.config import ureg from rct229.utils.compare_standard_val import ( compare_standard_val, compare_standard_val_strict, ) -from rct229.utils.std_comparisons import std_equal +from rct229.utils.std_comparisons import ( + std_equal, + std_equal_with_precision, + std_conservative_outcome, +) _M2 = ureg("m2") @@ -150,3 +155,72 @@ def test__compare_standard_val_strict_gt__false_with_units(): std_val=1.0101 * _M2, operator=operator.gt, ) + + +def test__std_equal_with_precision__false_types_vary(): + with pytest.raises(TypeError): + std_equal_with_precision(1.05 * _M2, 1.1, 0.1) + + +def test__std_equal_with_precision__true_with_units(): + assert std_equal_with_precision(1.05 * _M2, 1.1 * _M2, 0.1 * _M2) + + +def test__std_equal_with_precision__true_without_units(): + assert std_equal_with_precision(1.05, 1.1, 0.1) + + +def test__std_equal_with_precision__false_with_units(): + assert not std_equal_with_precision(1.15 * _M2, 1.1 * _M2, 0.1 * _M2) + + +def test__std_equal_with_precision__false_without_units(): + assert not std_equal_with_precision(1.15, 1.1, 0.1) + + +def test__std_equal_with_precision__10_true_with_units(): + assert std_equal_with_precision(145 * _M2, 150 * _M2, 10 * _M2) + + +def test__std_equal_with_precision__10_true_without_units(): + assert std_equal_with_precision(145, 150, 10) + + +def test__std_equal_with_precision__10_false_with_units(): + assert not std_equal_with_precision(155 * _M2, 150 * _M2, 10 * _M2) + + +def test__std_equal_with_precision__10_false_without_units(): + assert not std_equal_with_precision(155, 150, 10) + + +def test__std_conservative_outcome__true_with_units_gt(): + assert std_conservative_outcome(1.1 * _M2, 1.05 * _M2, operator.gt) + + +def test__std_conservative_outcome__true_with_units_lt(): + assert std_conservative_outcome(1.05 * _M2, 1.1 * _M2, operator.lt) + + +def test__std_conservative_outcome__false_with_units_gt(): + assert not std_conservative_outcome(1.09999 * _M2, 1.1 * _M2, operator.gt) + + +def test__std_conservative_outcome__false_with_units_lt(): + assert not std_conservative_outcome(1.05001 * _M2, 1.05 * _M2, operator.lt) + + +def test__std_conservative_outcome__true_without_units_gt(): + assert std_conservative_outcome(1.1, 1.05, operator.gt) + + +def test__std_conservative_outcome__true_without_units_lt(): + assert std_conservative_outcome(1.05, 1.1, operator.lt) + + +def test__std_conservative_outcome__false_without_units_gt(): + assert not std_conservative_outcome(1.09999, 1.1, operator.gt) + + +def test__std_conservative_outcome__false_without_units_lt(): + assert not std_conservative_outcome(1.05001, 1.05, operator.lt) diff --git a/rct229/web_application.py b/rct229/web_application.py index 4022a3fa8b..7bb30e85cd 100644 --- a/rct229/web_application.py +++ b/rct229/web_application.py @@ -121,7 +121,9 @@ def run_software_test(ruleset, section=None, saving_dir="./"): return report_dir -def run_project_evaluation(rpds, ruleset, reports=["RAW_OUTPUT"], saving_dir="./"): +def run_project_evaluation( + rpds, ruleset, reports=["RAW_OUTPUT"], saving_dir="./", session_id="" +): """ Parameters @@ -130,6 +132,7 @@ def run_project_evaluation(rpds, ruleset, reports=["RAW_OUTPUT"], saving_dir="./ ruleset: str ruleset key reports: list[str] list of strings and each string is the enum value of a report saving_dir: directory to save report. + session_id: a string representing a calculation session Returns ------- @@ -161,7 +164,7 @@ def run_project_evaluation(rpds, ruleset, reports=["RAW_OUTPUT"], saving_dir="./ print("Test implementation of rule engine for ASHRAE Std 229 RCT.") print("") - report = evaluate_all_rules_rpd(rpds) + report = evaluate_all_rules_rpd(rpds, session_id) print(f"Saving reports to: {saving_dir}......") report_path_list = [] diff --git a/ruleset-checking-tool-api.yaml b/ruleset-checking-tool-api.yaml new file mode 100644 index 0000000000..60f02aee4e --- /dev/null +++ b/ruleset-checking-tool-api.yaml @@ -0,0 +1,181 @@ +openapi: 3.0.3 +info: + title: Ruleset Checking Tool Web Service API + description: |- + + + termsOfService: NA + contact: + email: weili.xu@pnnl.gov + license: + name: MIT + url: https://github.com/pnnl/ruleset-checking-tool/blob/master/LICENSE + version: 1.0.11 +externalDocs: + description: Find out more about Ruleset Checking Tool + url: http://github.com/pnnl/ruleset-checking-tool +servers: + - url: https://n3y4h0an6g.execute-api.us-west-2.amazonaws.com/prod +paths: + /evaluate: + post: + summary: Run project evaluation on a set of RPDs + description: This function runs the ruleset evaluation. Parameters required include a string for ruleset, a string for desired reports written in a comma separate format and a zip file that contains all the required RPDs for evaluation. + parameters: + - name: ruleset + in: query + description: ruleset tag, for example ashrae9012019. + required: true + schema: + type: string + - name: reports + in: query + description: list of reports string in a command separated format, for example ASHRAE9012019DetailReport,RawSummary. + required: true + schema: + type: string + requestBody: + content: + application/octet-stream: + schema: + type: string + format: binary + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/Evaluation' + '400': + description: Invalid request, missing data. + '500': + description: Internal Error. + /rulesets: + get: + summary: Get a list of available ruleset tags + description: Get a list of available ruleset tags + responses: + '200': + description: Successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuleSetsCount' + '500': + description: Internal error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalError' + /rules: + get: + summary: Get number of rules implemented for a ruleset. + description: Get the number of rules for a specified ruleset. + parameters: + - name: ruleset + in: query + description: ruleset tag, for example ashrae9012019. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RulesCount' + '400': + description: Invalid request, missing ruleset. + content: + application/json: + schema: + $ref: '#/components/schemas/MissingDataError' + '500': + description: Internal error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalError' + /reports: + get: + summary: Get a list of available reports for a specified ruleset. + description: Get a list of available reports for a specified ruleset. + parameters: + - name: ruleset + in: query + description: ruleset tag, for example ashrae9012019. + required: true + schema: + type: string + responses: + '200': + description: successful operation + content: + application/json: + schema: + $ref: '#/components/schemas/RuleSetReports' + '400': + description: Invalid request, missing ruleset. + content: + application/json: + schema: + $ref: '#/components/schemas/MissingDataError' + '500': + description: Internal error. + content: + application/json: + schema: + $ref: '#/components/schemas/InternalError' +components: + schemas: + Evaluation: + type: object + properties: + sessionId: + type: string + example: "abc-def-efg" + RuleSetsCount: + type: object + properties: + num_rulesets: + type: integer + format: int64 + example: 10 + rulesets: + type: array + items: + type: string + example: ["ashrae9012019"] + RulesCount: + type: object + properties: + num_rules: + type: integer + format: int64 + example: 10 + RuleSetReports: + type: object + properties: + num_reports: + type: integer + format: int64 + example: 10 + reports: + type: array + items: + type: string + example: ["ASHRAE9012019DetailReport"] + MissingDataError: + type: object + properties: + message: + type: string + example: "Invalid request, missing rulesets." + InternalError: + type: object + properties: + message: + type: string + example: "Internal Error." \ No newline at end of file