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 14 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/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
"section5",
"section6",
"section10",
"section12",
"section16",
"section18",
"section19",
Expand Down
226 changes: 203 additions & 23 deletions rct229/rulesets/ashrae9012019/section12/section12rule3.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,30 @@
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 USER
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):
Expand All @@ -11,46 +33,204 @@ class Section12Rule3(RuleDefinitionListIndexedBase):
def __init__(self):
super(Section12Rule3, self).__init__(
rmds_used=produce_ruleset_model_description(
USER=True, BASELINE_0=False, PROPOSED=True
USER=False, BASELINE_0=True, PROPOSED=True
),
each_rule=Section12Rule3.BuildingRule(),
index_rmd=USER,
each_rule=Section12Rule3.RuleSetModelDescriptionRule(),
index_rmd=PROPOSED,
id="12-3",
description="User RMD Space ID in Proposed RMD",
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="Section Table G3.1-12 Receptacles: Modeling Requirements for the Proposed design",
standard_section="Table G3.1-12 Proposed Building Performance column",
is_primary_rule=True,
rmd_context="ruleset_model_descriptions/0/buildings",
list_path="ruleset_model_descriptions[0]",
required_fields={"$": ["calendar"], "$.calendar": ["is_leap_year"]},
data_items={"is_leap_year": (PROPOSED, "calendar/is_leap_year")},
)

class BuildingRule(RuleDefinitionListIndexedBase):
class RuleSetModelDescriptionRule(RuleDefinitionListIndexedBase):
def __init__(self):
super(Section12Rule3.BuildingRule, self).__init__(
super(Section12Rule3.RuleSetModelDescriptionRule, self).__init__(
rmds_used=produce_ruleset_model_description(
USER=True, BASELINE_0=False, PROPOSED=True
USER=False, BASELINE_0=True, PROPOSED=True
),
each_rule=Section12Rule3.BuildingRule.SpaceRule(),
index_rmd=USER,
list_path="$..spaces[*]", # All spaces in the building
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):
# Get the Proposed space id values
return {"proposed_space_ids": find_all("$..spaces[*].id", context.PROPOSED)}
rmd_b = context.BASELINE_0
rmd_p = context.PROPOSED

class SpaceRule(RuleDefinitionBase):
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.


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

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

return {
"user_space_id": context.USER["id"],
"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.

with the list path function in the RMD class, in here you can directly use space_p["lighting_space_type"]

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.

}

def rule_check(self, context, calc_vals, data):
return calc_vals["user_space_id"] in data["proposed_space_ids"]
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 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"]

if (
space_type_p not in EXPECTED_RECEPTACLE_CONTROL_SPACE_TYPES
and misc_equip_p["has_automatic_control"]
):
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do list path and is applicable functions, then you do not need this if statement here anymore.

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.

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]
]
Copy link
Collaborator

Choose a reason for hiding this comment

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

@JacksonJ-KC @KarenWGard
I do have a question here.
For a case when the hour_value in propose model is 1, how does it match to baseline case?
My suggestion would be flip the comparison in the RDS

expected_hourly_values_in_b = [
                            min(1, hour_value * (1 + expected_receptacle_power_credit))
                            for hour_value in schedule_p[hourly_multiplier_schedule_p]
                        ]
credit_comparison_data = compare_schedules(
                            expected_hourly_values_in_p,
                            schedule_b[hourly_multiplier_schedule_b],
                            mask_schedule,
                            is_leap_year,
                        )["total_hours_matched"]

Copy link
Collaborator

Choose a reason for hiding this comment

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

I am not following the question or the proposed change to the RDS. These are the 3 cases.

  1. comparison of proposed hourly values and expected proposed hourly values. If all hours match - pass.
  2. comparison of proposed hourly values and baseline hourly values. If all hours match - undetermined
  3. fail

The expected proposed hourly values are the baseline values * (1 - expected_receptacle_power_credit)

Copy link
Collaborator

@weilixu weilixu Aug 15, 2024

Choose a reason for hiding this comment

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

@JacksonJ-KC
Typically, user develop proposed model first then baseline model - this process also applies to any baseline automation systems.

If the multiplier value is 1.0 in an hour on a day in the proposed model, then the baseline value would be proposed values / (1 - expected_receptacle_power_credit), this sets the multiplier in baseline over the 1.0 threshold. In this scenario, how does the modeler set the multiplier in baseline?

Copy link
Collaborator

@JacksonJ-KC JacksonJ-KC Aug 15, 2024

Choose a reason for hiding this comment

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

If the proposed model used 1 as the multiplier value then that means that they forgot to apply the credit and they should fail the rule unless every hourly values matches the baseline and case 2 is met so they get undetermined.

Copy link
Collaborator

Choose a reason for hiding this comment

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

image

Copy link
Collaborator

Choose a reason for hiding this comment

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

Note this can still lead to expected values that are greater than 1 and you should not replace the expected values greater than 1 with 1. If an expected value is greater than 1 then either Case 2 is met or Fail.

Copy link
Collaborator

@weilixu weilixu Aug 16, 2024

Choose a reason for hiding this comment

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

I think this would be a problem since it relates to software capabilities and circular reference in the standard.
I would suggest adding an undetermined for scenario where when resetting multiplier > 1.0 to 1.0 matches to proposed and add a message such as "Hours matched only when resetting greater than 1.0 hourly multipliers to 1.0"
This case is different from CASE 2 since a good chunk of hours would be matching the credit_comparison_data but only a portion of those are over 1.0.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Note to discuss at the next RDS meeting

Copy link
Collaborator

@weilixu weilixu Aug 16, 2024

Choose a reason for hiding this comment

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

The next RDS meeting won't happen in two weeks so I opened an issue #1510 for the discussion later.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I'll address this issue when the issue is resolved.


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_data": credit_comparison_data,
Copy link
Collaborator

Choose a reason for hiding this comment

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

this is confusing - use credit_comparison_total_hours_matched and no_credit_comparison_total_hours_matched

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.

"no_credit_comparison_data": 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_data = calc_vals["no_credit_comparison_data"]
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_data
== 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_data = calc_vals["credit_comparison_data"]
hourly_multiplier_schedule_len_p = calc_vals[
"hourly_multiplier_schedule_len_p"
]

return (
credit_comparison_data
== hourly_multiplier_schedule_len_p
== expected_hourly_values_len
)
Loading
Loading