Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RCT/YJ/Rule 12-3 #1409

Merged
merged 13 commits into from
Aug 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rct229/rulesets/ashrae9012019/section12/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

__all__ = [
"section12rule1",
"section12rule3",
"section12rule4",
]

Expand Down
244 changes: 244 additions & 0 deletions rct229/rulesets/ashrae9012019/section12/section12rule3.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
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.rule_engine.rulesets import LeapYear
from rct229.rulesets.ashrae9012019 import PROPOSED
from rct229.rulesets.ashrae9012019.ruleset_functions.compare_schedules import (
compare_schedules,
)
from rct229.schema.schema_enums import SchemaEnums
from rct229.utils.assertions import getattr_
from rct229.utils.jsonpath_utils import find_all
from rct229.utils.utility_functions import find_exactly_one_schedule

LIGHTING_SPACE = SchemaEnums.schema_enums["LightingSpaceOptions2019ASHRAE901TG37"]


EXPECTED_RECEPTACLE_CONTROL_SPACE_TYPES = [
LIGHTING_SPACE.OFFICE_ENCLOSED,
LIGHTING_SPACE.CONFERENCE_MEETING_MULTIPURPOSE_ROOM,
LIGHTING_SPACE.COPY_PRINT_ROOM,
LIGHTING_SPACE.LOUNGE_BREAKROOM_HEALTH_CARE_FACILITY,
LIGHTING_SPACE.LOUNGE_BREAKROOM_ALL_OTHERS,
LIGHTING_SPACE.CLASSROOM_LECTURE_HALL_TRAINING_ROOM_PENITENTIARY,
LIGHTING_SPACE.CLASSROOM_LECTURE_HALL_TRAINING_ROOM_SCHOOL,
LIGHTING_SPACE.CLASSROOM_LECTURE_HALL_TRAINING_ROOM_ALL_OTHER,
LIGHTING_SPACE.OFFICE_OPEN_PLAN,
]


class Section12Rule3(RuleDefinitionListIndexedBase):
"""Rule 3 of ASHRAE 90.1-2019 Appendix G Section 12 (Receptacle)"""

def __init__(self):
super(Section12Rule3, self).__init__(
rmds_used=produce_ruleset_model_description(
USER=False, BASELINE_0=True, PROPOSED=True
),
each_rule=Section12Rule3.RuleSetModelDescriptionRule(),
index_rmd=PROPOSED,
id="12-3",
description="When receptacle controls are specified in the proposed building design for spaces where not required by Standard 90.1 2019 Section 8.4.2, "
"the hourly receptacle schedule shall be reduced as specified in Standard 90.1-2019 Table G3.1 Section 12 Proposed Building Performance column.",
ruleset_section_title="Receptacle",
standard_section="Table G3.1-12 Proposed Building Performance column",
is_primary_rule=True,
list_path="ruleset_model_descriptions[0]",
required_fields={"$": ["calendar"], "$.calendar": ["is_leap_year"]},
data_items={"is_leap_year": (PROPOSED, "calendar/is_leap_year")},
)

class RuleSetModelDescriptionRule(RuleDefinitionListIndexedBase):
def __init__(self):
super(Section12Rule3.RuleSetModelDescriptionRule, self).__init__(
rmds_used=produce_ruleset_model_description(
USER=False, BASELINE_0=True, PROPOSED=True
),
each_rule=Section12Rule3.RuleSetModelDescriptionRule.SpaceRule(),
index_rmd=PROPOSED,
list_path="$.buildings[*].building_segments[*].zones[*].spaces[*]",
)

def is_applicable(self, context, data=None):
rmd_p = context.PROPOSED

spaces_with_receptacle_controls_beyond_req = []
for space_p in find_all(
"$.buildings[*].building_segments[*].zones[*].spaces[*]",
rmd_p,
):
lighting_space_type_p = getattr_(
space_p, "spaces", "lighting_space_type"
)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use space_p.get("lighting_space_type", None) instead of getattr_.
Main reason is this getattr_ will trigger UNDETERMINED outcome, which may cause this rule to be undetermined every time.

Also, None should be sufficient for lighting_space_type_p not in EXPECTED_RECEPTACLE_CONTROL_SPACE_TYPES logic.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Addressed.

if lighting_space_type_p not in EXPECTED_RECEPTACLE_CONTROL_SPACE_TYPES:
for misc_equip_p in find_all(
"$.miscellaneous_equipment[*]", space_p
):
if misc_equip_p.get("has_automatic_control"):
spaces_with_receptacle_controls_beyond_req.append(
misc_equip_p["id"]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is ok to change from RDS, but the name of the parameter should be updated to misc_equip_receptacle_controls_beyond_req

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed.

)

return spaces_with_receptacle_controls_beyond_req

def create_data(self, context, data):
rmd_b = context.BASELINE_0
rmd_p = context.PROPOSED

schedule_b = {
mult_sch_b: find_exactly_one_schedule(rmd_b, mult_sch_b)[
"hourly_values"
]
for mult_sch_b in find_all(
"$.buildings[*].building_segments[*].zones[*].spaces[*].miscellaneous_equipment[*].multiplier_schedule",
rmd_b,
)
}
schedule_p = {
mult_sch_p: find_exactly_one_schedule(rmd_p, mult_sch_p)[
"hourly_values"
]
for mult_sch_p in find_all(
"$.buildings[*].building_segments[*].zones[*].spaces[*].miscellaneous_equipment[*].multiplier_schedule",
rmd_p,
)
}

return {"schedule_b": schedule_b, "schedule_p": schedule_p}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You will need a list path to filter out the spaces in the expected receptacle control space types here.
space_type_p in EXPECTED_RECEPTACLE_CONTROL_SPACE_TYPES, skip evaluation since these cases are evaluated in Rule 12-2

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't this already considered in the is_applicable method above? I see if lighting_space_type_p not in EXPECTED_RECEPTACLE_CONTROL_SPACE_TYPES: in line 71.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The is_applicable checks if ANY spaces in the RPD has receptacle_controls_beyond_req. If there is none, then this rule is not applicable.

In here, the purpose to add list_path is to filter out the spaces that are not in EXPECTED_RECEPTACLE_CONTROL_SPACE_TYPES for the next step processing. Those spaces will be tested in Rule 12-2 anyway. If no list_path, those spaces will fail the evaluation.

Check it again, the list_path shall be under the RuleSetModelDescriptionRule class, not SpaceRule class.


def list_filter(self, context_item, data):
space_p = context_item.PROPOSED
lighting_space_type_p = getattr_(space_p, "spaces", "lighting_space_type")
return lighting_space_type_p not in EXPECTED_RECEPTACLE_CONTROL_SPACE_TYPES

class SpaceRule(RuleDefinitionListIndexedBase):
def __init__(self):
super(
Section12Rule3.RuleSetModelDescriptionRule.SpaceRule, self
).__init__(
rmds_used=produce_ruleset_model_description(
USER=False, BASELINE_0=True, PROPOSED=True
),
each_rule=Section12Rule3.RuleSetModelDescriptionRule.SpaceRule.MiscEquipRule(),
index_rmd=PROPOSED,
list_path="$.miscellaneous_equipment[*]",
)

def create_data(self, context, data):
space_p = context.PROPOSED

return {"space_type_p": space_p["lighting_space_type"]}

class MiscEquipRule(RuleDefinitionBase):
def __init__(self):
super(
Section12Rule3.RuleSetModelDescriptionRule.SpaceRule.MiscEquipRule,
self,
).__init__(
rmds_used=produce_ruleset_model_description(
USER=False, BASELINE_0=True, PROPOSED=True
),
required_fields={
"$": [
"has_automatic_control",
"multiplier_schedule",
]
},
manual_check_required_msg="Credit for automatic receptacle controls was expected, but baseline and proposed miscellaneous equipment schedules are identical.",
)

def is_applicable(self, context, data=None):
misc_equip_p = context.PROPOSED
return misc_equip_p.get("has_automatic_control")

def get_not_applicable_msg(self, context, data=None):
misc_equip_p = context.PROPOSED
space_type_p = data["space_type_p"]
return f"Misc equipment {misc_equip_p['id']} is in a {space_type_p} space but it does not has automatic control."

def get_calc_vals(self, context, data=None):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should do a not applicable here.
The main reason to have not applicable is to filter out the misc equipment that has auto_receptacle_control_p = False
We can attach a not applicable message say Misc equipment {misc_equip_id} does not has automatic control in the model.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! Addressed.

misc_equip_b = context.BASELINE_0
misc_equip_p = context.PROPOSED

is_leap_year = data["is_leap_year"]
space_type_p = data["space_type_p"]
schedule_b = data["schedule_b"]
schedule_p = data["schedule_p"]

expected_receptacle_power_credit = 0.1 * getattr_(
misc_equip_p,
"miscellaneous_equipment",
"automatic_controlled_percentage",
)

hourly_multiplier_schedule_b = misc_equip_b["multiplier_schedule"]
hourly_multiplier_schedule_p = misc_equip_p["multiplier_schedule"]

expected_hourly_values = [
hour_value * (1 - expected_receptacle_power_credit)
for hour_value in schedule_b[hourly_multiplier_schedule_b]
]

mask_schedule = (
[1] * LeapYear.LEAP_YEAR_HOURS
if is_leap_year
else [1] * LeapYear.REGULAR_YEAR_HOURS
)

credit_comparison_data = compare_schedules(
expected_hourly_values,
schedule_p[hourly_multiplier_schedule_p],
mask_schedule,
is_leap_year,
)["total_hours_matched"]

no_credit_comparison_data = compare_schedules(
schedule_b[hourly_multiplier_schedule_b],
schedule_p[hourly_multiplier_schedule_p],
mask_schedule,
is_leap_year,
)["total_hours_matched"]

return {
"expected_hourly_values_len": len(expected_hourly_values),
"credit_comparison_total_hours_matched": credit_comparison_data,
"no_credit_comparison_total_hours_matched": no_credit_comparison_data,
"hourly_multiplier_schedule_len_b": len(
schedule_b[hourly_multiplier_schedule_b]
),
"hourly_multiplier_schedule_len_p": len(
schedule_p[hourly_multiplier_schedule_p]
),
}

def manual_check_required(self, context, calc_vals=None, data=None):
no_credit_comparison_total_hours_matched = calc_vals[
"no_credit_comparison_total_hours_matched"
]
hourly_multiplier_schedule_len_b = calc_vals[
"hourly_multiplier_schedule_len_b"
]
hourly_multiplier_schedule_len_p = calc_vals[
"hourly_multiplier_schedule_len_p"
]

return (
no_credit_comparison_total_hours_matched
== hourly_multiplier_schedule_len_b
== hourly_multiplier_schedule_len_p
)

def rule_check(self, context, calc_vals=None, data=None):
expected_hourly_values_len = calc_vals["expected_hourly_values_len"]
credit_comparison_total_hours_matched = calc_vals[
"credit_comparison_total_hours_matched"
]
hourly_multiplier_schedule_len_p = calc_vals[
"hourly_multiplier_schedule_len_p"
]

return (
credit_comparison_total_hours_matched
== hourly_multiplier_schedule_len_p
== expected_hourly_values_len
)
Loading
Loading