From 7f211e334d87c44b69f523d60e0f545fccee33bd Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Sun, 24 Sep 2023 18:43:07 -0700 Subject: [PATCH 01/11] local loop 2.5 --- .../LocalLoopSaturationDirectActingMax.py | 45 ++++ src/library/LocalLoopSetPointTracking.py | 28 +++ src/library/LocalLoopUnmetHours.py | 75 ++++++ src/library/__init__.py | 9 + tests/test_localloop_SetPointTracking.py | 235 ++++++++++++++++++ tests/test_localloop_UnmetHours.py | 185 ++++++++++++++ 6 files changed, 577 insertions(+) create mode 100644 src/library/LocalLoopSaturationDirectActingMax.py create mode 100644 src/library/LocalLoopSetPointTracking.py create mode 100644 src/library/LocalLoopUnmetHours.py create mode 100644 tests/test_localloop_SetPointTracking.py create mode 100644 tests/test_localloop_UnmetHours.py diff --git a/src/library/LocalLoopSaturationDirectActingMax.py b/src/library/LocalLoopSaturationDirectActingMax.py new file mode 100644 index 00000000..2d9dfbc4 --- /dev/null +++ b/src/library/LocalLoopSaturationDirectActingMax.py @@ -0,0 +1,45 @@ +import pandas as pd +from checklib import RulesCheckBase + +class LocalLoopSaturationDirectActingMax(RuleCheckBase): + points = ["feedback_senosr", "set_point", "cmd_max"] + + def saturation_flag(self, t): + if 0 <= t['cmd_max'] - t['feedback_sensor'] < 0.01: + return True + else: + return False + + def err_above_flag(self, t): + if t['feedback_sensor'] - t['set_point'] > 0: + return True + else: + return False + + def verify(self): + self.saturation = self.df.apply(lambda t: self.saturation_flag(t), axis=1) + self.err_above = self.df.apply(lambda t: self.err_above_flag(t), axis=1) + self.consistent_err_flag = pd.Series(index=self.df.index) + prev_time = None + prev = None + err_start_time = None + first_flag = True + err_time = 0 + for cur_time, cur in self.df.iterrows(): + if self.err_above.loc[cur_time]: + if err_start_time is None: + err_start_time = cur_time + else: + err_time = (cur_time - err_start_time).total_seconds() / 3600 # in hours + else: + err_start_time = None + err_time = 0 + + if first_flag: + self.consistent_err_flag.loc[cur_time] = False + first_flag = False + else: + pass + + prev_time = cur_time + prev = cur diff --git a/src/library/LocalLoopSetPointTracking.py b/src/library/LocalLoopSetPointTracking.py new file mode 100644 index 00000000..10be1867 --- /dev/null +++ b/src/library/LocalLoopSetPointTracking.py @@ -0,0 +1,28 @@ +from checklib import RuleCheckBase + + +class LocalLoopSetPointTracking(RuleCheckBase): + points = ["feedback_sensor", "set_point"] + + def error_below_5percent(self, t): + # this method checks each sample, and returns true if the error is within 5 percent of absolute setpoint value + # if the set point is 0, a default error threshold of 0.01 is used + err_abs = abs(t["feedback_sensor"] - t["set_point"]) + if t["set_point"] == 0: + if err_abs > 0.01: + return False + else: + return True + if err_abs / abs(t["set_point"]) > 0.05: + return False + else: + return True + + def verify(self): + self.result = self.df.apply(lambda t: self.error_below_5percent(t), axis=1) + + def check_bool(self): + if len(self.result[self.result == False]) / len(self.result) > 0.05: + return False + else: + return True diff --git a/src/library/LocalLoopUnmetHours.py b/src/library/LocalLoopUnmetHours.py new file mode 100644 index 00000000..32a37950 --- /dev/null +++ b/src/library/LocalLoopUnmetHours.py @@ -0,0 +1,75 @@ +import pandas as pd +from checklib import RuleCheckBase + + +class LocalLoopUnmetHours(RuleCheckBase): + points = ["feedback_sensor", "set_point"] + + def error_below_5percent(self, t): + # this method checks each sample, and returns true if the error is within 5 percent of absolute setpoint value + # if the set point is 0, a default error threshold of 0.01 is used + err_abs = abs(t["feedback_sensor"] - t["set_point"]) + if t["set_point"] == 0: + if err_abs > 0.01: + return False + else: + return True + if err_abs / abs(t["set_point"]) > 0.05: + return False + else: + return True + + def time_error_below_5percent(self, cur, prev, cur_time, prev_time): + if prev is None: + return 0 + + if (not self.error_below_5percent(cur)) and ( + not self.error_below_5percent(prev) + ): + time_delta = cur_time - prev_time + hour_change = time_delta.total_seconds() / 3600 + return hour_change + else: + return 0 + + def verify(self): + self.result = self.df.apply(lambda t: self.error_below_5percent(t), axis=1) + self.unmethours_ts = pd.Series(index=self.df.index) + prev_time = None + prev = None + first_flag = True + for cur_time, cur in self.df.iterrows(): + if first_flag: + self.unmethours_ts.loc[cur_time] = 0 + first_flag = False + else: + self.unmethours_ts.loc[cur_time] = self.time_error_below_5percent( + cur, prev, cur_time, prev_time + ) + prev_time = cur_time + prev = cur + + self.total_unmet_hours = sum(self.unmethours_ts) + self.total_hours = ( + self.unmethours_ts.index[-1] - self.unmethours_ts.index[0] + ).total_seconds() / 3600 + + def check_bool(self): + if self.total_unmet_hours / self.total_hours > 0.05: + return False + else: + return True + + def check_detail(self): + print("Verification results dict: ") + output = { + "Sample #": len(self.result), + "Pass #": len(self.result[self.result == True]), + "Fail #": len(self.result[self.result == False]), + "Verification Passed?": self.check_bool(), + "Total Data Duration Hours": self.total_hours, + "Total Unmet Hours": self.total_unmet_hours, + "Total Unmet Hours Ratio": self.total_unmet_hours / self.total_hours, + } + print(output) + return output diff --git a/src/library/__init__.py b/src/library/__init__.py index e955a762..5cd75ca8 100644 --- a/src/library/__init__.py +++ b/src/library/__init__.py @@ -28,6 +28,8 @@ from .G36FreezeProtectionStage1 import * from .G36FreezeProtectionStage2 import * from .G36FreezeProtectionStage3 import * +from .LocalLoopSetPointTracking import * +from .LocalLoopUnmetHours import * __all__ = [ "AutomaticOADamperControl", @@ -63,4 +65,11 @@ "G36FreezeProtectionStage1", "G36FreezeProtectionStage2", "G36FreezeProtectionStage3", + "LocalLoopSetPointTracking", + "LocalLoopUnmetHours", + # "LocalLoopSaturationDirectActingMaximum", + # "LocalLoopSaturationDirectActingMinimum", + # "LocalLoopSaturationReverseActingMaximum", + # "LocalLoopSaturationReverseActingMinimum", + # "LocalLoopHuntingActivation", ] diff --git a/tests/test_localloop_SetPointTracking.py b/tests/test_localloop_SetPointTracking.py new file mode 100644 index 00000000..5d1bbc4a --- /dev/null +++ b/tests/test_localloop_SetPointTracking.py @@ -0,0 +1,235 @@ +import unittest, sys +import datetime + +sys.path.append("./src") +from lib_unit_test_runner import * +from library import * +import pandas as pd + + +class TestLocalLoopSetPointTracking(unittest.TestCase): + def test_set_point_tracking_pass(self): + points = ["feedback_sensor", "set_point"] + timestamp = [ + datetime(2023, 5, 1, 18, 0, 0), + datetime(2023, 5, 1, 18, 1, 0), + datetime(2023, 5, 1, 18, 2, 0), + datetime(2023, 5, 1, 18, 3, 0), + datetime(2023, 5, 1, 18, 4, 0), + datetime(2023, 5, 1, 18, 5, 0), + datetime(2023, 5, 1, 18, 6, 0), + datetime(2023, 5, 1, 18, 7, 0), + datetime(2023, 5, 1, 18, 8, 0), + datetime(2023, 5, 1, 18, 9, 0), + datetime(2023, 5, 1, 18, 10, 0), + datetime(2023, 5, 1, 18, 11, 0), + datetime(2023, 5, 1, 18, 12, 0), + datetime(2023, 5, 1, 18, 13, 0), + datetime(2023, 5, 1, 18, 14, 0), + datetime(2023, 5, 1, 18, 15, 0), + datetime(2023, 5, 1, 18, 16, 0), + datetime(2023, 5, 1, 18, 17, 0), + datetime(2023, 5, 1, 18, 18, 0), + datetime(2023, 5, 1, 18, 19, 0), + datetime(2023, 5, 1, 18, 20, 0), + ] + + data = [ + [96, 100], + [97, 100], + [98, 100], + [99, 100], + [100, 100], + [101, 100], + [102, 100], + [103, 100], + [104, 100], + [105.1, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series([ + True, + True, + True, + True, + True, + True, + True, + True, + True, + False, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ]) + + verification_obj = run_test_verification_with_data( + "LocalLoopSetPointTracking", df + ) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + + self.assertTrue(results.equals(expected_results)) + self.assertTrue(binaryflag) + + def test_set_point_tracking_fail_samplepct(self): + points = ["feedback_sensor", "set_point"] + timestamp = [ + datetime(2023, 5, 1, 18, 0, 0), + datetime(2023, 5, 1, 18, 1, 0), + datetime(2023, 5, 1, 18, 2, 0), + datetime(2023, 5, 1, 18, 3, 0), + datetime(2023, 5, 1, 18, 4, 0), + datetime(2023, 5, 1, 18, 5, 0), + datetime(2023, 5, 1, 18, 6, 0), + datetime(2023, 5, 1, 18, 7, 0), + datetime(2023, 5, 1, 18, 8, 0), + datetime(2023, 5, 1, 18, 9, 0), + ] + + data = [ + [96, 100], + [97, 100], + [98, 100], + [99, 100], + [100, 100], + [101, 100], + [102, 100], + [103, 100], + [104, 100], + [105.1, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series([ + True, + True, + True, + True, + True, + True, + True, + True, + True, + False, + ]) + + verification_obj = run_test_verification_with_data( + "LocalLoopSetPointTracking", df + ) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + + self.assertTrue(results.equals(expected_results)) + self.assertFalse(binaryflag) + + def test_set_point_tracking_pass(self): + points = ["feedback_sensor", "set_point"] + timestamp = [ + datetime(2023, 5, 1, 18, 0, 0), + datetime(2023, 5, 1, 18, 1, 0), + datetime(2023, 5, 1, 18, 2, 0), + datetime(2023, 5, 1, 18, 3, 0), + datetime(2023, 5, 1, 18, 4, 0), + datetime(2023, 5, 1, 18, 5, 0), + datetime(2023, 5, 1, 18, 6, 0), + datetime(2023, 5, 1, 18, 7, 0), + datetime(2023, 5, 1, 18, 8, 0), + datetime(2023, 5, 1, 18, 9, 0), + datetime(2023, 5, 1, 18, 10, 0), + datetime(2023, 5, 1, 18, 11, 0), + datetime(2023, 5, 1, 18, 12, 0), + datetime(2023, 5, 1, 18, 13, 0), + datetime(2023, 5, 1, 18, 14, 0), + datetime(2023, 5, 1, 18, 15, 0), + datetime(2023, 5, 1, 18, 16, 0), + datetime(2023, 5, 1, 18, 17, 0), + datetime(2023, 5, 1, 18, 18, 0), + datetime(2023, 5, 1, 18, 19, 0), + datetime(2023, 5, 1, 18, 20, 0), + ] + + data = [ + [96, 100], + [97, 100], + [98, 100], + [99, 100], + [100, 100], + [101, 100], + [102, 100], + [103, 100], + [104, 100], + [105.1, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series([ + True, + True, + True, + True, + True, + True, + True, + True, + True, + False, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ]) + + verification_obj = run_test_verification_with_data( + "LocalLoopSetPointTracking", df + ) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + + self.assertTrue(results.equals(expected_results)) + self.assertTrue(binaryflag) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_localloop_UnmetHours.py b/tests/test_localloop_UnmetHours.py new file mode 100644 index 00000000..08002e18 --- /dev/null +++ b/tests/test_localloop_UnmetHours.py @@ -0,0 +1,185 @@ +import unittest, sys +import datetime + +sys.path.append("./src") +from lib_unit_test_runner import * +from library import * +import pandas as pd + + +class TestLocalLoopUnmetHours(unittest.TestCase): + def test_unmet_hours_pass(self): + points = ["feedback_sensor", "set_point"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 20), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 10, 5, 0), + datetime(2023, 5, 1, 11, 5, 0), + datetime(2023, 5, 1, 12, 5, 0), + datetime(2023, 5, 1, 13, 5, 0), + datetime(2023, 5, 1, 14, 5, 0), + datetime(2023, 5, 1, 15, 5, 0), + datetime(2023, 5, 1, 16, 5, 0), + datetime(2023, 5, 1, 17, 5, 0), + datetime(2023, 5, 1, 18, 5, 0), + datetime(2023, 5, 1, 19, 5, 0), + datetime(2023, 5, 1, 20, 5, 0), + ] + + data = [ + [96, 100], + [97, 100], + [98, 100], + [99, 100], + [100, 100], + [101, 100], + [102, 100], + [103, 100], + [106, 100], + [105.1, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + False, + False, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopUnmetHours", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertTrue(binaryflag) + + def test_unmet_hours_pass(self): + points = ["feedback_sensor", "set_point"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 20), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 7, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 10, 5, 0), + datetime(2023, 5, 1, 11, 5, 0), + datetime(2023, 5, 1, 12, 5, 0), + datetime(2023, 5, 1, 13, 5, 0), + datetime(2023, 5, 1, 14, 5, 0), + datetime(2023, 5, 1, 15, 5, 0), + datetime(2023, 5, 1, 16, 5, 0), + datetime(2023, 5, 1, 17, 5, 0), + datetime(2023, 5, 1, 18, 5, 0), + datetime(2023, 5, 1, 19, 5, 0), + datetime(2023, 5, 1, 20, 5, 0), + ] + + data = [ + [96, 100], + [97, 100], + [98, 100], + [99, 100], + [100, 100], + [101, 100], + [102, 100], + [103, 100], + [106, 100], + [105.1, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + [104, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + False, + False, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopUnmetHours", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertFalse(binaryflag) + + +if __name__ == "__main__": + unittest.main() From 2f48a79aac3c559a95c9f2de6f11644ab52e5f5b Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Sun, 24 Sep 2023 21:30:33 -0700 Subject: [PATCH 02/11] saturation da max added --- .../LocalLoopSaturationDirectActingMax.py | 35 +++-- src/library/__init__.py | 4 +- ...est_localloop_SaturationDirectActingMax.py | 124 ++++++++++++++++++ 3 files changed, 143 insertions(+), 20 deletions(-) create mode 100644 tests/test_localloop_SaturationDirectActingMax.py diff --git a/src/library/LocalLoopSaturationDirectActingMax.py b/src/library/LocalLoopSaturationDirectActingMax.py index 2d9dfbc4..edece022 100644 --- a/src/library/LocalLoopSaturationDirectActingMax.py +++ b/src/library/LocalLoopSaturationDirectActingMax.py @@ -1,45 +1,44 @@ import pandas as pd -from checklib import RulesCheckBase +from checklib import RuleCheckBase + class LocalLoopSaturationDirectActingMax(RuleCheckBase): - points = ["feedback_senosr", "set_point", "cmd_max"] + points = ["feedback_sensor", "set_point", "cmd", "cmd_max"] def saturation_flag(self, t): - if 0 <= t['cmd_max'] - t['feedback_sensor'] < 0.01: + if 0 <= t["cmd_max"] - t["cmd"] <= 0.01: return True else: return False - def err_above_flag(self, t): - if t['feedback_sensor'] - t['set_point'] > 0: + def err_flag(self, t): + if t["feedback_sensor"] - t["set_point"] > 0: return True else: return False def verify(self): self.saturation = self.df.apply(lambda t: self.saturation_flag(t), axis=1) - self.err_above = self.df.apply(lambda t: self.err_above_flag(t), axis=1) - self.consistent_err_flag = pd.Series(index=self.df.index) - prev_time = None - prev = None + self.err = self.df.apply(lambda t: self.err_flag(t), axis=1) + self.result = pd.Series(index=self.df.index) err_start_time = None first_flag = True err_time = 0 for cur_time, cur in self.df.iterrows(): - if self.err_above.loc[cur_time]: + if self.err.loc[cur_time]: if err_start_time is None: err_start_time = cur_time else: - err_time = (cur_time - err_start_time).total_seconds() / 3600 # in hours - else: + err_time = ( + cur_time - err_start_time + ).total_seconds() / 3600 # in hours + else: # reset err_start_time = None err_time = 0 - if first_flag: - self.consistent_err_flag.loc[cur_time] = False - first_flag = False + if err_time > 1 and (not self.saturation.loc[cur_time]): + result_flag = False else: - pass + result_flag = True - prev_time = cur_time - prev = cur + self.result.loc[cur_time] = result_flag diff --git a/src/library/__init__.py b/src/library/__init__.py index 5cd75ca8..8d1bcccf 100644 --- a/src/library/__init__.py +++ b/src/library/__init__.py @@ -30,7 +30,7 @@ from .G36FreezeProtectionStage3 import * from .LocalLoopSetPointTracking import * from .LocalLoopUnmetHours import * - +from .LocalLoopSaturationDirectActingMax import * __all__ = [ "AutomaticOADamperControl", "AutomaticShutdown", @@ -67,7 +67,7 @@ "G36FreezeProtectionStage3", "LocalLoopSetPointTracking", "LocalLoopUnmetHours", - # "LocalLoopSaturationDirectActingMaximum", + "LocalLoopSaturationDirectActingMax" # "LocalLoopSaturationDirectActingMinimum", # "LocalLoopSaturationReverseActingMaximum", # "LocalLoopSaturationReverseActingMinimum", diff --git a/tests/test_localloop_SaturationDirectActingMax.py b/tests/test_localloop_SaturationDirectActingMax.py new file mode 100644 index 00000000..38402bee --- /dev/null +++ b/tests/test_localloop_SaturationDirectActingMax.py @@ -0,0 +1,124 @@ +import unittest, sys +import datetime + +sys.path.append("./src") +from lib_unit_test_runner import * +from library import * +import pandas as pd + + +class TestLocalLoopSaturationDirectActingMax(unittest.TestCase): + def test_saturation_damax_pass(self): + points = ["feedback_sensor", "set_point", "cmd", "cmd_max"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 0), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 10, 5, 0), + ] + + data = [ + [100, 100, 50, 100], + [106, 100, 99.99, 100], + [107, 100, 100, 100], + [103, 100, 100, 100], + [102, 100, 100, 100], + [103, 100, 100, 100], + [103, 100, 100, 100], + [99, 100, 98, 100], + [100, 100, 55, 100], + [100, 100, 30, 100], + [100, 100, 100, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopSaturationDirectActingMax", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertTrue(binaryflag) + + def test_saturation_damax_pass(self): + points = ["feedback_sensor", "set_point", "cmd", "cmd_max"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 0), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 11, 5, 0), + ] + + data = [ + [100, 100, 50, 100], + [106, 100, 99.99, 100], + [107, 100, 99, 100], + [103, 100, 99, 100], + [102, 100, 100, 100], + [103, 100, 100, 100], + [103, 100, 100, 100], + [99, 100, 98, 100], + [100, 100, 55, 100], + [101, 100, 30, 100], + [101, 100, 90, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + False, + True, + True, + True, + True, + True, + True, + False, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopSaturationDirectActingMax", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertFalse(binaryflag) + +if __name__ == "__main__": + unittest.main() From 21462498fa0d2ff0c1c2ab5dbd24475e5dd0b235 Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Mon, 25 Sep 2023 00:09:40 -0700 Subject: [PATCH 03/11] add all saturation items --- .../LocalLoopSaturationDirectActingMin.py | 43 ++++++ .../LocalLoopSaturationReverseActingMax.py | 43 ++++++ .../LocalLoopSaturationReverseActingMin.py | 43 ++++++ src/library/__init__.py | 12 +- ...est_localloop_SaturationDirectActingMin.py | 124 ++++++++++++++++++ ...st_localloop_SaturationReverseActingMax.py | 124 ++++++++++++++++++ ...st_localloop_SaturationReverseActingMin.py | 124 ++++++++++++++++++ 7 files changed, 509 insertions(+), 4 deletions(-) create mode 100644 src/library/LocalLoopSaturationDirectActingMin.py create mode 100644 src/library/LocalLoopSaturationReverseActingMax.py create mode 100644 src/library/LocalLoopSaturationReverseActingMin.py create mode 100644 tests/test_localloop_SaturationDirectActingMin.py create mode 100644 tests/test_localloop_SaturationReverseActingMax.py create mode 100644 tests/test_localloop_SaturationReverseActingMin.py diff --git a/src/library/LocalLoopSaturationDirectActingMin.py b/src/library/LocalLoopSaturationDirectActingMin.py new file mode 100644 index 00000000..ff2b5fc5 --- /dev/null +++ b/src/library/LocalLoopSaturationDirectActingMin.py @@ -0,0 +1,43 @@ +import pandas as pd +from checklib import RuleCheckBase + + +class LocalLoopSaturationDirectActingMin(RuleCheckBase): + points = ["feedback_sensor", "set_point", "cmd", "cmd_min"] + + def saturation_flag(self, t): + if 0 <= t["cmd"] - t['cmd_min'] <= 0.01: + return True + else: + return False + + def err_flag(self, t): + if t["feedback_sensor"] < t["set_point"]: + return True + else: + return False + + def verify(self): + self.saturation = self.df.apply(lambda t: self.saturation_flag(t), axis=1) + self.err = self.df.apply(lambda t: self.err_flag(t), axis=1) + self.result = pd.Series(index=self.df.index) + err_start_time = None + err_time = 0 + for cur_time, cur in self.df.iterrows(): + if self.err.loc[cur_time]: + if err_start_time is None: + err_start_time = cur_time + else: + err_time = ( + cur_time - err_start_time + ).total_seconds() / 3600 # in hours + else: # reset + err_start_time = None + err_time = 0 + + if err_time > 1 and (not self.saturation.loc[cur_time]): + result_flag = False + else: + result_flag = True + + self.result.loc[cur_time] = result_flag diff --git a/src/library/LocalLoopSaturationReverseActingMax.py b/src/library/LocalLoopSaturationReverseActingMax.py new file mode 100644 index 00000000..36f5c660 --- /dev/null +++ b/src/library/LocalLoopSaturationReverseActingMax.py @@ -0,0 +1,43 @@ +import pandas as pd +from checklib import RuleCheckBase + + +class LocalLoopSaturationReverseActingMax(RuleCheckBase): + points = ["feedback_sensor", "set_point", "cmd", "cmd_max"] + + def saturation_flag(self, t): + if 0 <= t["cmd_max"] - t["cmd"] <= 0.01: + return True + else: + return False + + def err_flag(self, t): + if t["feedback_sensor"] < t["set_point"]: + return True + else: + return False + + def verify(self): + self.saturation = self.df.apply(lambda t: self.saturation_flag(t), axis=1) + self.err = self.df.apply(lambda t: self.err_flag(t), axis=1) + self.result = pd.Series(index=self.df.index) + err_start_time = None + err_time = 0 + for cur_time, cur in self.df.iterrows(): + if self.err.loc[cur_time]: + if err_start_time is None: + err_start_time = cur_time + else: + err_time = ( + cur_time - err_start_time + ).total_seconds() / 3600 # in hours + else: # reset + err_start_time = None + err_time = 0 + + if err_time > 1 and (not self.saturation.loc[cur_time]): + result_flag = False + else: + result_flag = True + + self.result.loc[cur_time] = result_flag diff --git a/src/library/LocalLoopSaturationReverseActingMin.py b/src/library/LocalLoopSaturationReverseActingMin.py new file mode 100644 index 00000000..9a22a584 --- /dev/null +++ b/src/library/LocalLoopSaturationReverseActingMin.py @@ -0,0 +1,43 @@ +import pandas as pd +from checklib import RuleCheckBase + + +class LocalLoopSaturationReverseActingMin(RuleCheckBase): + points = ["feedback_sensor", "set_point", "cmd", "cmd_min"] + + def saturation_flag(self, t): + if 0 <= t["cmd"] - t['cmd_min'] <= 0.01: + return True + else: + return False + + def err_flag(self, t): + if t["feedback_sensor"] > t["set_point"]: + return True + else: + return False + + def verify(self): + self.saturation = self.df.apply(lambda t: self.saturation_flag(t), axis=1) + self.err = self.df.apply(lambda t: self.err_flag(t), axis=1) + self.result = pd.Series(index=self.df.index) + err_start_time = None + err_time = 0 + for cur_time, cur in self.df.iterrows(): + if self.err.loc[cur_time]: + if err_start_time is None: + err_start_time = cur_time + else: + err_time = ( + cur_time - err_start_time + ).total_seconds() / 3600 # in hours + else: # reset + err_start_time = None + err_time = 0 + + if err_time > 1 and (not self.saturation.loc[cur_time]): + result_flag = False + else: + result_flag = True + + self.result.loc[cur_time] = result_flag diff --git a/src/library/__init__.py b/src/library/__init__.py index 8d1bcccf..b49480a8 100644 --- a/src/library/__init__.py +++ b/src/library/__init__.py @@ -31,6 +31,10 @@ from .LocalLoopSetPointTracking import * from .LocalLoopUnmetHours import * from .LocalLoopSaturationDirectActingMax import * +from .LocalLoopSaturationDirectActingMin import * +from .LocalLoopSaturationReverseActingMax import * +from .LocalLoopSaturationReverseActingMin import * + __all__ = [ "AutomaticOADamperControl", "AutomaticShutdown", @@ -67,9 +71,9 @@ "G36FreezeProtectionStage3", "LocalLoopSetPointTracking", "LocalLoopUnmetHours", - "LocalLoopSaturationDirectActingMax" - # "LocalLoopSaturationDirectActingMinimum", - # "LocalLoopSaturationReverseActingMaximum", - # "LocalLoopSaturationReverseActingMinimum", + "LocalLoopSaturationDirectActingMax", + "LocalLoopSaturationDirectActingMin", + "LocalLoopSaturationReverseActingMax", + "LocalLoopSaturationReverseActingMin", # "LocalLoopHuntingActivation", ] diff --git a/tests/test_localloop_SaturationDirectActingMin.py b/tests/test_localloop_SaturationDirectActingMin.py new file mode 100644 index 00000000..5e0bd812 --- /dev/null +++ b/tests/test_localloop_SaturationDirectActingMin.py @@ -0,0 +1,124 @@ +import unittest, sys +import datetime + +sys.path.append("./src") +from lib_unit_test_runner import * +from library import * +import pandas as pd + + +class TestLocalLoopSaturationDirectActingMin(unittest.TestCase): + def test_saturation_damin_pass(self): + points = ["feedback_sensor", "set_point", "cmd", "cmd_min"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 0), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 10, 5, 0), + ] + + data = [ + [100, 100, 50, 0], + [94, 100, 0.01, 0], + [93, 100, 0, 0], + [97, 100, 0, 0], + [98, 100, 0, 0], + [97, 100, 0, 0], + [97, 100, 0, 0], + [101, 100, 2, 0], + [100, 100, 50, 0], + [100, 100, 30, 0], + [100, 100, 0, 0], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopSaturationDirectActingMin", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertTrue(binaryflag) + + def test_saturation_damax_pass(self): + points = ["feedback_sensor", "set_point", "cmd", "cmd_min"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 0), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 11, 5, 0), + ] + + data = [ + [100, 100, 50, 0], + [94, 100, 0.01, 0], + [93, 100, 1, 0], + [97, 100, 1, 0], + [98, 100, 0, 0], + [97, 100, 0, 0], + [97, 100, 0, 0], + [101, 100, 2, 0], + [100, 100, 55, 0], + [99, 100, 30, 0], + [99, 100, 90, 0], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + False, + True, + True, + True, + True, + True, + True, + False, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopSaturationDirectActingMin", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertFalse(binaryflag) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_localloop_SaturationReverseActingMax.py b/tests/test_localloop_SaturationReverseActingMax.py new file mode 100644 index 00000000..f7957f0e --- /dev/null +++ b/tests/test_localloop_SaturationReverseActingMax.py @@ -0,0 +1,124 @@ +import unittest, sys +import datetime + +sys.path.append("./src") +from lib_unit_test_runner import * +from library import * +import pandas as pd + + +class TestLocalLoopSaturationReverseActingMax(unittest.TestCase): + def test_saturation_ramax_pass(self): + points = ["feedback_sensor", "set_point", "cmd", "cmd_max"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 0), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 10, 5, 0), + ] + + data = [ + [100, 100, 50, 100], + [94, 100, 99.99, 100], + [93, 100, 100, 100], + [97, 100, 100, 100], + [98, 100, 100, 100], + [97, 100, 100, 100], + [97, 100, 100, 100], + [101, 100, 98, 100], + [100, 100, 55, 100], + [100, 100, 30, 100], + [100, 100, 100, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopSaturationReverseActingMax", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertTrue(binaryflag) + + def test_saturation_damax_pass(self): + points = ["feedback_sensor", "set_point", "cmd", "cmd_max"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 0), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 11, 5, 0), + ] + + data = [ + [100, 100, 50, 100], + [94, 100, 99.99, 100], + [93, 100, 99, 100], + [97, 100, 99, 100], + [98, 100, 100, 100], + [97, 100, 100, 100], + [97, 100, 100, 100], + [101, 100, 98, 100], + [100, 100, 55, 100], + [99, 100, 30, 100], + [99, 100, 90, 100], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + False, + True, + True, + True, + True, + True, + True, + False, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopSaturationReverseActingMax", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertFalse(binaryflag) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_localloop_SaturationReverseActingMin.py b/tests/test_localloop_SaturationReverseActingMin.py new file mode 100644 index 00000000..fcc248cb --- /dev/null +++ b/tests/test_localloop_SaturationReverseActingMin.py @@ -0,0 +1,124 @@ +import unittest, sys +import datetime + +sys.path.append("./src") +from lib_unit_test_runner import * +from library import * +import pandas as pd + + +class TestLocalLoopSaturationReverseActingMin(unittest.TestCase): + def test_saturation_ramin_pass(self): + points = ["feedback_sensor", "set_point", "cmd", "cmd_min"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 0), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 10, 5, 0), + ] + + data = [ + [100, 100, 50, 0], + [106, 100, 0.01, 0], + [107, 100, 0, 0], + [103, 100, 0, 0], + [102, 100, 0, 0], + [103, 100, 0, 0], + [103, 100, 0, 0], + [99, 100, 2, 0], + [100, 100, 55, 0], + [100, 100, 30, 0], + [100, 100, 0, 0], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopSaturationReverseActingMin", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertTrue(binaryflag) + + def test_saturation_ramin_fail(self): + points = ["feedback_sensor", "set_point", "cmd", "cmd_min"] + timestamp = [ + datetime(2023, 5, 1, 0, 5, 0), + datetime(2023, 5, 1, 1, 5, 10), + datetime(2023, 5, 1, 2, 5, 0), + datetime(2023, 5, 1, 3, 5, 30), + datetime(2023, 5, 1, 4, 5, 0), + datetime(2023, 5, 1, 5, 5, 0), + datetime(2023, 5, 1, 6, 5, 0), + datetime(2023, 5, 1, 7, 5, 0), + datetime(2023, 5, 1, 8, 5, 5), + datetime(2023, 5, 1, 9, 5, 0), + datetime(2023, 5, 1, 11, 5, 0), + ] + + data = [ + [100, 100, 50, 0], + [106, 100, 0.01, 0], + [107, 100, 1, 0], + [103, 100, 1, 0], + [102, 100, 0, 0], + [103, 100, 0, 0], + [103, 100, 0, 0], + [99, 100, 2, 0], + [100, 100, 55, 0], + [101, 100, 30, 0], + [101, 100, 90, 0], + ] + + df = pd.DataFrame(data, columns=points, index=timestamp) + expected_results = pd.Series( + [ + True, + True, + True, + False, + True, + True, + True, + True, + True, + True, + False, + ] + ) + + verification_obj = run_test_verification_with_data("LocalLoopSaturationReverseActingMin", df) + + results = pd.Series(list(verification_obj.result)) + binaryflag = verification_obj.check_bool() + verification_obj.check_detail() + + self.assertTrue(results.equals(expected_results)) + self.assertFalse(binaryflag) + +if __name__ == "__main__": + unittest.main() From 4850bfc01a221f0bcc2f12c0145e05f2ae6b851e Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Mon, 25 Sep 2023 00:11:31 -0700 Subject: [PATCH 04/11] formatting --- src/library/LocalLoopSaturationDirectActingMax.py | 2 +- src/library/LocalLoopSaturationDirectActingMin.py | 2 +- src/library/LocalLoopSaturationReverseActingMin.py | 2 +- tests/test_localloop_SaturationDirectActingMax.py | 9 +++++++-- tests/test_localloop_SaturationDirectActingMin.py | 9 +++++++-- tests/test_localloop_SaturationReverseActingMax.py | 9 +++++++-- tests/test_localloop_SaturationReverseActingMin.py | 9 +++++++-- 7 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/library/LocalLoopSaturationDirectActingMax.py b/src/library/LocalLoopSaturationDirectActingMax.py index edece022..a4fcf485 100644 --- a/src/library/LocalLoopSaturationDirectActingMax.py +++ b/src/library/LocalLoopSaturationDirectActingMax.py @@ -12,7 +12,7 @@ def saturation_flag(self, t): return False def err_flag(self, t): - if t["feedback_sensor"] - t["set_point"] > 0: + if t["feedback_sensor"] > t["set_point"]: return True else: return False diff --git a/src/library/LocalLoopSaturationDirectActingMin.py b/src/library/LocalLoopSaturationDirectActingMin.py index ff2b5fc5..339bd996 100644 --- a/src/library/LocalLoopSaturationDirectActingMin.py +++ b/src/library/LocalLoopSaturationDirectActingMin.py @@ -6,7 +6,7 @@ class LocalLoopSaturationDirectActingMin(RuleCheckBase): points = ["feedback_sensor", "set_point", "cmd", "cmd_min"] def saturation_flag(self, t): - if 0 <= t["cmd"] - t['cmd_min'] <= 0.01: + if 0 <= t["cmd"] - t["cmd_min"] <= 0.01: return True else: return False diff --git a/src/library/LocalLoopSaturationReverseActingMin.py b/src/library/LocalLoopSaturationReverseActingMin.py index 9a22a584..c94254a6 100644 --- a/src/library/LocalLoopSaturationReverseActingMin.py +++ b/src/library/LocalLoopSaturationReverseActingMin.py @@ -6,7 +6,7 @@ class LocalLoopSaturationReverseActingMin(RuleCheckBase): points = ["feedback_sensor", "set_point", "cmd", "cmd_min"] def saturation_flag(self, t): - if 0 <= t["cmd"] - t['cmd_min'] <= 0.01: + if 0 <= t["cmd"] - t["cmd_min"] <= 0.01: return True else: return False diff --git a/tests/test_localloop_SaturationDirectActingMax.py b/tests/test_localloop_SaturationDirectActingMax.py index 38402bee..99edf354 100644 --- a/tests/test_localloop_SaturationDirectActingMax.py +++ b/tests/test_localloop_SaturationDirectActingMax.py @@ -55,7 +55,9 @@ def test_saturation_damax_pass(self): ] ) - verification_obj = run_test_verification_with_data("LocalLoopSaturationDirectActingMax", df) + verification_obj = run_test_verification_with_data( + "LocalLoopSaturationDirectActingMax", df + ) results = pd.Series(list(verification_obj.result)) binaryflag = verification_obj.check_bool() @@ -111,7 +113,9 @@ def test_saturation_damax_pass(self): ] ) - verification_obj = run_test_verification_with_data("LocalLoopSaturationDirectActingMax", df) + verification_obj = run_test_verification_with_data( + "LocalLoopSaturationDirectActingMax", df + ) results = pd.Series(list(verification_obj.result)) binaryflag = verification_obj.check_bool() @@ -120,5 +124,6 @@ def test_saturation_damax_pass(self): self.assertTrue(results.equals(expected_results)) self.assertFalse(binaryflag) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_localloop_SaturationDirectActingMin.py b/tests/test_localloop_SaturationDirectActingMin.py index 5e0bd812..67da85a8 100644 --- a/tests/test_localloop_SaturationDirectActingMin.py +++ b/tests/test_localloop_SaturationDirectActingMin.py @@ -55,7 +55,9 @@ def test_saturation_damin_pass(self): ] ) - verification_obj = run_test_verification_with_data("LocalLoopSaturationDirectActingMin", df) + verification_obj = run_test_verification_with_data( + "LocalLoopSaturationDirectActingMin", df + ) results = pd.Series(list(verification_obj.result)) binaryflag = verification_obj.check_bool() @@ -111,7 +113,9 @@ def test_saturation_damax_pass(self): ] ) - verification_obj = run_test_verification_with_data("LocalLoopSaturationDirectActingMin", df) + verification_obj = run_test_verification_with_data( + "LocalLoopSaturationDirectActingMin", df + ) results = pd.Series(list(verification_obj.result)) binaryflag = verification_obj.check_bool() @@ -120,5 +124,6 @@ def test_saturation_damax_pass(self): self.assertTrue(results.equals(expected_results)) self.assertFalse(binaryflag) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_localloop_SaturationReverseActingMax.py b/tests/test_localloop_SaturationReverseActingMax.py index f7957f0e..2aaf79b9 100644 --- a/tests/test_localloop_SaturationReverseActingMax.py +++ b/tests/test_localloop_SaturationReverseActingMax.py @@ -55,7 +55,9 @@ def test_saturation_ramax_pass(self): ] ) - verification_obj = run_test_verification_with_data("LocalLoopSaturationReverseActingMax", df) + verification_obj = run_test_verification_with_data( + "LocalLoopSaturationReverseActingMax", df + ) results = pd.Series(list(verification_obj.result)) binaryflag = verification_obj.check_bool() @@ -111,7 +113,9 @@ def test_saturation_damax_pass(self): ] ) - verification_obj = run_test_verification_with_data("LocalLoopSaturationReverseActingMax", df) + verification_obj = run_test_verification_with_data( + "LocalLoopSaturationReverseActingMax", df + ) results = pd.Series(list(verification_obj.result)) binaryflag = verification_obj.check_bool() @@ -120,5 +124,6 @@ def test_saturation_damax_pass(self): self.assertTrue(results.equals(expected_results)) self.assertFalse(binaryflag) + if __name__ == "__main__": unittest.main() diff --git a/tests/test_localloop_SaturationReverseActingMin.py b/tests/test_localloop_SaturationReverseActingMin.py index fcc248cb..2d2525a7 100644 --- a/tests/test_localloop_SaturationReverseActingMin.py +++ b/tests/test_localloop_SaturationReverseActingMin.py @@ -55,7 +55,9 @@ def test_saturation_ramin_pass(self): ] ) - verification_obj = run_test_verification_with_data("LocalLoopSaturationReverseActingMin", df) + verification_obj = run_test_verification_with_data( + "LocalLoopSaturationReverseActingMin", df + ) results = pd.Series(list(verification_obj.result)) binaryflag = verification_obj.check_bool() @@ -111,7 +113,9 @@ def test_saturation_ramin_fail(self): ] ) - verification_obj = run_test_verification_with_data("LocalLoopSaturationReverseActingMin", df) + verification_obj = run_test_verification_with_data( + "LocalLoopSaturationReverseActingMin", df + ) results = pd.Series(list(verification_obj.result)) binaryflag = verification_obj.check_bool() @@ -120,5 +124,6 @@ def test_saturation_ramin_fail(self): self.assertTrue(results.equals(expected_results)) self.assertFalse(binaryflag) + if __name__ == "__main__": unittest.main() From 7cc164fe3d88e34219df38c16ba4aaafbbb2cdf8 Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Mon, 25 Sep 2023 00:15:05 -0700 Subject: [PATCH 05/11] black --- tests/test_localloop_SetPointTracking.py | 122 ++++++++++++----------- 1 file changed, 64 insertions(+), 58 deletions(-) diff --git a/tests/test_localloop_SetPointTracking.py b/tests/test_localloop_SetPointTracking.py index 5d1bbc4a..1196dc53 100644 --- a/tests/test_localloop_SetPointTracking.py +++ b/tests/test_localloop_SetPointTracking.py @@ -59,29 +59,31 @@ def test_set_point_tracking_pass(self): ] df = pd.DataFrame(data, columns=points, index=timestamp) - expected_results = pd.Series([ - True, - True, - True, - True, - True, - True, - True, - True, - True, - False, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - ]) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + True, + False, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ] + ) verification_obj = run_test_verification_with_data( "LocalLoopSetPointTracking", df @@ -122,18 +124,20 @@ def test_set_point_tracking_fail_samplepct(self): ] df = pd.DataFrame(data, columns=points, index=timestamp) - expected_results = pd.Series([ - True, - True, - True, - True, - True, - True, - True, - True, - True, - False, - ]) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + True, + False, + ] + ) verification_obj = run_test_verification_with_data( "LocalLoopSetPointTracking", df @@ -196,29 +200,31 @@ def test_set_point_tracking_pass(self): ] df = pd.DataFrame(data, columns=points, index=timestamp) - expected_results = pd.Series([ - True, - True, - True, - True, - True, - True, - True, - True, - True, - False, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - True, - ]) + expected_results = pd.Series( + [ + True, + True, + True, + True, + True, + True, + True, + True, + True, + False, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + True, + ] + ) verification_obj = run_test_verification_with_data( "LocalLoopSetPointTracking", df From 697aaa8ff974d36acc8d58ccfdb647f4f065d6d2 Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Mon, 25 Sep 2023 02:50:15 -0700 Subject: [PATCH 06/11] add docstring --- .../LocalLoopSaturationDirectActingMax.py | 20 +++++++++++++++++++ .../LocalLoopSaturationDirectActingMin.py | 20 +++++++++++++++++++ .../LocalLoopSaturationReverseActingMax.py | 20 +++++++++++++++++++ .../LocalLoopSaturationReverseActingMin.py | 20 +++++++++++++++++++ src/library/LocalLoopSetPointTracking.py | 18 +++++++++++++++++ src/library/LocalLoopUnmetHours.py | 20 +++++++++++++++++++ 6 files changed, 118 insertions(+) diff --git a/src/library/LocalLoopSaturationDirectActingMax.py b/src/library/LocalLoopSaturationDirectActingMax.py index a4fcf485..cecf92f8 100644 --- a/src/library/LocalLoopSaturationDirectActingMax.py +++ b/src/library/LocalLoopSaturationDirectActingMax.py @@ -1,3 +1,23 @@ +""" +## Local Loop Performance Verification - Direct Acting Loop Actuator Maximum Saturation + +### Description + +This verification checks that a direct acting control loop would saturate its actuator to maximum when the error is consistently above the set point. + +### Verification logic + +If the sensed data values are consistently above its set point, and after a default of 1 hour, the control command is still not saturated to maximum, then the verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value +- cmd : control command +- cmd_max: control command range maximum value + +""" + import pandas as pd from checklib import RuleCheckBase diff --git a/src/library/LocalLoopSaturationDirectActingMin.py b/src/library/LocalLoopSaturationDirectActingMin.py index 339bd996..3389d905 100644 --- a/src/library/LocalLoopSaturationDirectActingMin.py +++ b/src/library/LocalLoopSaturationDirectActingMin.py @@ -1,3 +1,23 @@ +""" +## Local Loop Performance Verification - Direct Acting Loop Actuator Minimum Saturation + +### Description + +This verification checks that a direct acting control loop would saturate its actuator to minimum when the error is consistently below the set point. + +### Verification logic + +If the sensed data values are consistently below its set point, and after a default of 1 hour, the control command is still not saturated to minimum, then the verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value +- cmd : control command +- cmd_min: control command range minimum value + +""" + import pandas as pd from checklib import RuleCheckBase diff --git a/src/library/LocalLoopSaturationReverseActingMax.py b/src/library/LocalLoopSaturationReverseActingMax.py index 36f5c660..3b599256 100644 --- a/src/library/LocalLoopSaturationReverseActingMax.py +++ b/src/library/LocalLoopSaturationReverseActingMax.py @@ -1,3 +1,23 @@ +""" +## Local Loop Performance Verification - Reverse Acting Loop Actuator Maximum Saturation + +### Description + +This verification checks that a reverse acting control loop would saturate its actuator to maximum when the error is consistently below the set point. + +### Verification logic + +If the sensed data values are consistently below its set point, and after a default of 1 hour, the control command is still not saturated to maximum, then the verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value +- cmd : control command +- cmd_max: control command range maximum value + +""" + import pandas as pd from checklib import RuleCheckBase diff --git a/src/library/LocalLoopSaturationReverseActingMin.py b/src/library/LocalLoopSaturationReverseActingMin.py index c94254a6..0670aa27 100644 --- a/src/library/LocalLoopSaturationReverseActingMin.py +++ b/src/library/LocalLoopSaturationReverseActingMin.py @@ -1,3 +1,23 @@ +""" +## Local Loop Performance Verification - Reverse Acting Loop Actuator Minimum Saturation + +### Description + +This verification checks that a reverse acting control loop would saturate its actuator to minimum when the error is consistently above the set point. + +### Verification logic + +If the sensed data values are consistently above its set point, and after a default of 1 hour, the control command is still not saturated to minimum, then the verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value +- cmd : control command +- cmd_min: control command range minimum value + +""" + import pandas as pd from checklib import RuleCheckBase diff --git a/src/library/LocalLoopSetPointTracking.py b/src/library/LocalLoopSetPointTracking.py index 10be1867..7b1063b5 100644 --- a/src/library/LocalLoopSetPointTracking.py +++ b/src/library/LocalLoopSetPointTracking.py @@ -1,3 +1,21 @@ +""" +## Local Loop Performance Verification - Set Point Tracking + +### Description + +This verification checks the set point tracking ability of local control loops. + +### Verification logic + +With a threshold of 5% of abs(set_point) (if the set point is 0, then the threshold is default to be 0.01), if the number of samples of which the error is larger than this threshold is beyond 5% of number of all samples, then this verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value + +""" + from checklib import RuleCheckBase diff --git a/src/library/LocalLoopUnmetHours.py b/src/library/LocalLoopUnmetHours.py index 32a37950..8425b6fa 100644 --- a/src/library/LocalLoopUnmetHours.py +++ b/src/library/LocalLoopUnmetHours.py @@ -1,3 +1,23 @@ +""" +## Local Loop Performance Verification - Set Point Unmet Hours + +### Description + +This verification checks the set point tracking ability of local control loops. + +### Verification logic + +Instead of checking the number of samples among the whole data set for which the set points are not met, this verification checks the total accumulated time that the set points are not met within a threshold of 5% of abs(set_point) (if the set point is 0, then the threshold is default to be 0.01). + +If the accumulated time of unmet set point is beyond 5% of the whole duration the data covers, then this verification fails; otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value + +""" + import pandas as pd from checklib import RuleCheckBase From 7c0d71a64612afcc44dde117b6b28dc90945da0c Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Mon, 25 Sep 2023 02:52:11 -0700 Subject: [PATCH 07/11] add design doc --- design/local_loop_lib_contents.md | 120 ++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 design/local_loop_lib_contents.md diff --git a/design/local_loop_lib_contents.md b/design/local_loop_lib_contents.md new file mode 100644 index 00000000..ad65e7db --- /dev/null +++ b/design/local_loop_lib_contents.md @@ -0,0 +1,120 @@ +# Local Loop Performance Verification Library Items + +---- + +## Local Loop Performance Verification - Set Point Tracking + +### Description + +This verification checks the set point tracking ability of local control loops. + +### Verification logic + +With a threshold of 5% of abs(set_point) (if the set point is 0, then the threshold is default to be 0.01), if the number of samples of which the error is larger than this threshold is beyond 5% of number of all samples, then this verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value + + +## Local Loop Performance Verification - Set Point Unmet Hours + +### Description + +This verification checks the set point tracking ability of local control loops. + +### Verification logic + +Instead of checking the number of samples among the whole data set for which the set points are not met, this verification checks the total accumulated time that the set points are not met within a threshold of 5% of abs(set_point) (if the set point is 0, then the threshold is default to be 0.01). + +If the accumulated time of unmet set point is beyond 5% of the whole duration the data covers, then this verification fails; otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value + +## Local Loop Performance Verification - Direct Acting Loop Actuator Maximum Saturation + +### Description + +This verification checks that a direct acting control loop would saturate its actuator to maximum when the error is consistently above the set point [^1]. + +### Verification logic + +If the sensed data values are consistently above its set point, and after a default of 1 hour, the control command is still not saturated to maximum, then the verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value +- cmd : control command +- cmd_max: control command range maximum value + +## Local Loop Performance Verification - Direct Acting Loop Actuator Minimum Saturation + +### Description + +This verification checks that a direct acting control loop would saturate its actuator to minimum when the error is consistently below the set point [^1]. + +### Verification logic + +If the sensed data values are consistently below its set point, and after a default of 1 hour, the control command is still not saturated to minimum, then the verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value +- cmd : control command +- cmd_min: control command range minimum value + +## Local Loop Performance Verification - Reverse Acting Loop Actuator Maximum Saturation + +### Description + +This verification checks that a reverse acting control loop would saturate its actuator to maximum when the error is consistently below the set point [^1]. + +### Verification logic + +If the sensed data values are consistently below its set point, and after a default of 1 hour, the control command is still not saturated to maximum, then the verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value +- cmd : control command +- cmd_max: control command range maximum value + +## Local Loop Performance Verification - Reverse Acting Loop Actuator Minimum Saturation + +### Description + +This verification checks that a reverse acting control loop would saturate its actuator to minimum when the error is consistently above the set point [^1]. + +### Verification logic + +If the sensed data values are consistently above its set point, and after a default of 1 hour, the control command is still not saturated to minimum, then the verification fails; Otherwise, it passes. + +### Data requirements + +- feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point +- set_point: set point value +- cmd : control command +- cmd_min: control command range minimum value + + + +## Local Loop Performance Verification - Actuator Rate of Change + +### Description + +This verification checks if a local loop actuator has dramatic change of its actuating command. This verification is implemented as instructed by ASHRAE Guideline 36 2021 Section 5.1.9 in Verification Item `G36OutputChangeRateLimit`. + + + +[^1]: Lei, Xuechen, Yan Chen, Mario Bergés, and Burcu Akinci. "Formalized control logic fault definition with ontological reasoning for air handling units." Automation in Construction 129 (2021): 103781. \ No newline at end of file From 838f8c09a3933e1070874167786b7dcf1ba5ecf8 Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Sat, 30 Sep 2023 18:13:17 -0700 Subject: [PATCH 08/11] add lib items json --- schema/library.json | 86 +++++++++++++++++++ .../LocalLoopSaturationDirectActingMax.py | 2 +- .../LocalLoopSaturationDirectActingMin.py | 2 +- .../LocalLoopSaturationReverseActingMax.py | 2 +- .../LocalLoopSaturationReverseActingMin.py | 2 +- 5 files changed, 90 insertions(+), 4 deletions(-) diff --git a/schema/library.json b/schema/library.json index 5d3942fb..f1c8adc7 100644 --- a/schema/library.json +++ b/schema/library.json @@ -1055,5 +1055,91 @@ ], "description_verification_type": "rule-based", "assertions_type": "pass" + }, + "LocalLoopSetPointTracking": { + "library_item_id": 47, + "description_brief": "Local Loop Performance Verification - Set Point Tracking", + "description_datapoints": { + "feedback_sensor": "feedback sensor reading of the subject to be controlled towards a set point", + "set_point": "set point value" + }, + "description_assertions": [ + "With a threshold of 5% of abs(set_point) (if the set point is 0, then the threshold is default to be 0.01), if the number of samples of which the error is larger than this threshold is beyond 5% of number of all samples, then this verification fails; Otherwise, it passes." + ], + "description_verification_type": "procedure-based", + "assertions_type": "pass" + }, + "LocalLoopUnmetHours": { + "library_item_id": 48, + "description_brief": "Local Loop Performance Verification - Set Point Unmet Hours", + "description_datapoints": { + "feedback_sensor": "feedback sensor reading of the subject to be controlled towards a set point", + "set_point": "set point value" + }, + "description_assertions": [ + "Instead of checking the number of samples among the whole data set for which the set points are not met, this verification checks the total accumulated time that the set points are not met within a threshold of 5% of abs(set_point) (if the set point is 0, then the threshold is default to be 0.01)." + ], + "description_verification_type": "procedure-based", + "assertions_type": "pass" + }, + "LocalLoopSaturationDirectActingMax": { + "library_item_id": 49, + "description_brief": "Local Loop Performance Verification - Direct Acting Loop Actuator Maximum Saturation", + "description_datapoints": { + "feedback_sensor": "feedback sensor reading of the subject to be controlled towards a set point", + "set_point": "set point value", + "cmd": "control command", + "cmd_max": "control command range maximum value" + }, + "description_assertions": [ + "If the sensed data values are consistently above its set point, and after a default of 1 hour, the control command is still not saturated to maximum, then the verification fails; Otherwise, it passes." + ], + "description_verification_type": "procedure-based", + "assertions_type": "pass" + }, + "LocalLoopSaturationDirectActingMin": { + "library_item_id": 50, + "description_brief": "Local Loop Performance Verification - Direct Acting Loop Actuator Minimum Saturation", + "description_datapoints": { + "feedback_sensor": "feedback sensor reading of the subject to be controlled towards a set point", + "set_point": "set point value", + "cmd": "control command", + "cmd_min": "control command range minimum value" + }, + "description_assertions": [ + "If the sensed data values are consistently below its set point, and after a default of 1 hour, the control command is still not saturated to minimum, then the verification fails; Otherwise, it passes." + ], + "description_verification_type": "procedure-based", + "assertions_type": "pass" + }, + "LocalLoopSaturationReverseActingMax": { + "library_item_id": 51, + "description_brief": "Local Loop Performance Verification - Reverse Acting Loop Actuator Maximum Saturation", + "description_datapoints": { + "feedback_sensor": "feedback sensor reading of the subject to be controlled towards a set point", + "set_point": "set point value", + "cmd": "control command", + "cmd_max": "control command range maximum value" + }, + "description_assertions": [ + "If the sensed data values are consistently below its set point, and after a default of 1 hour, the control command is still not saturated to maximum, then the verification fails; Otherwise, it passes." + ], + "description_verification_type": "procedure-based", + "assertions_type": "pass" + }, + "LocalLoopSaturationReverseActingMin": { + "library_item_id": 52, + "description_brief": "Local Loop Performance Verification - Reverse Acting Loop Actuator Minimum Saturation", + "description_datapoints": { + "feedback_sensor": "feedback sensor reading of the subject to be controlled towards a set point", + "set_point": "set point value", + "cmd": "control command", + "cmd_min": "control command range minimum value" + }, + "description_assertions": [ + "If the sensed data values are consistently above its set point, and after a default of 1 hour, the control command is still not saturated to minimum, then the verification fails; Otherwise, it passes." + ], + "description_verification_type": "procedure-based", + "assertions_type": "pass" } } \ No newline at end of file diff --git a/src/library/LocalLoopSaturationDirectActingMax.py b/src/library/LocalLoopSaturationDirectActingMax.py index cecf92f8..457ee038 100644 --- a/src/library/LocalLoopSaturationDirectActingMax.py +++ b/src/library/LocalLoopSaturationDirectActingMax.py @@ -13,7 +13,7 @@ - feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point - set_point: set point value -- cmd : control command +- cmd: control command - cmd_max: control command range maximum value """ diff --git a/src/library/LocalLoopSaturationDirectActingMin.py b/src/library/LocalLoopSaturationDirectActingMin.py index 3389d905..86a21d62 100644 --- a/src/library/LocalLoopSaturationDirectActingMin.py +++ b/src/library/LocalLoopSaturationDirectActingMin.py @@ -13,7 +13,7 @@ - feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point - set_point: set point value -- cmd : control command +- cmd: control command - cmd_min: control command range minimum value """ diff --git a/src/library/LocalLoopSaturationReverseActingMax.py b/src/library/LocalLoopSaturationReverseActingMax.py index 3b599256..64018c0c 100644 --- a/src/library/LocalLoopSaturationReverseActingMax.py +++ b/src/library/LocalLoopSaturationReverseActingMax.py @@ -13,7 +13,7 @@ - feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point - set_point: set point value -- cmd : control command +- cmd: control command - cmd_max: control command range maximum value """ diff --git a/src/library/LocalLoopSaturationReverseActingMin.py b/src/library/LocalLoopSaturationReverseActingMin.py index 0670aa27..b9fc3de0 100644 --- a/src/library/LocalLoopSaturationReverseActingMin.py +++ b/src/library/LocalLoopSaturationReverseActingMin.py @@ -13,7 +13,7 @@ - feedback_sensor: feedback sensor reading of the subject to be controlled towards a set point - set_point: set point value -- cmd : control command +- cmd: control command - cmd_min: control command range minimum value """ From 0d6c097684c52ac930a1ea392f38e8c0f761416e Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Sat, 30 Sep 2023 19:09:55 -0700 Subject: [PATCH 09/11] respond to pr comment --- src/library/LocalLoopSaturationDirectActingMax.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/library/LocalLoopSaturationDirectActingMax.py b/src/library/LocalLoopSaturationDirectActingMax.py index 457ee038..04e408ef 100644 --- a/src/library/LocalLoopSaturationDirectActingMax.py +++ b/src/library/LocalLoopSaturationDirectActingMax.py @@ -42,7 +42,6 @@ def verify(self): self.err = self.df.apply(lambda t: self.err_flag(t), axis=1) self.result = pd.Series(index=self.df.index) err_start_time = None - first_flag = True err_time = 0 for cur_time, cur in self.df.iterrows(): if self.err.loc[cur_time]: From 4b3a787a227219184fbaffdd14274f172fb91a51 Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Sat, 30 Sep 2023 19:16:57 -0700 Subject: [PATCH 10/11] upgrade readme --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2511c15e..e6dbaa19 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,8 @@ See the Publications section for more information and example of uses of the fra - Demos are located in `demo/` - Visit [API documentation page](https://pnnl.github.io/ConStrain/) to learn about how to use the ConStrain API. - Visit [Guideline 36 Verification Items List](./design/g36_lib_contents.md) to learn more about the ASHRAE Guideline 36 related verification in ConStrain verification library. +- Visit [Local Loop Verification Items List](./design/local_loop_verification_items_list.md) to learn more about local loop performance verification library. +- Visit [Brick Integration Doc](./design/brick_integration_doc.md) to learn more about the beta version of brick schema integration API. From c8cf91d2003c8a304e6446439323f9ccaf56c8c0 Mon Sep 17 00:00:00 2001 From: "Xuechen (Jerry) Lei" Date: Sat, 30 Sep 2023 19:17:37 -0700 Subject: [PATCH 11/11] update citation.cff before release --- CITATION.cff | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CITATION.cff b/CITATION.cff index fb29d12b..dedaec0b 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -39,5 +39,5 @@ abstract: >- ANIMATE) license: BSD-2-Clause commit: a8b74df506f141d7840403a06a255959157bcc1e -version: 0.1.0 -date-released: '2023-03-31' \ No newline at end of file +version: 0.3.0 +date-released: '2023-09-30' \ No newline at end of file