From 7f01c55200c2257e43b227764e642d5a0b72fa8a Mon Sep 17 00:00:00 2001 From: Doyle Rowland Date: Fri, 11 Oct 2024 01:15:03 -0400 Subject: [PATCH] refactor: criticality calculations (#1412) * refactor: criticality functions * test: update and add tests to cover criticality function changes --- src/ramstk/analyses/criticality.py | 74 +++++++++---------- tests/analyses/test_criticality.py | 114 +++++++++++++++++++---------- 2 files changed, 110 insertions(+), 78 deletions(-) diff --git a/src/ramstk/analyses/criticality.py b/src/ramstk/analyses/criticality.py index ff179b5c4..1be1efc50 100644 --- a/src/ramstk/analyses/criticality.py +++ b/src/ramstk/analyses/criticality.py @@ -6,11 +6,14 @@ # Copyright 2019 Doyle Rowland doyle.rowland reliaqual com """FMEA Criticality Analysis Module.""" +# Standard Library Imports +from typing import Dict, Union + # RAMSTK Package Imports from ramstk.exceptions import OutOfRangeError -def calculate_rpn(sod): +def calculate_rpn(sod: Dict[str, int]) -> int: """Calculate the Risk Priority Number (RPN). RPN = S * O * D @@ -33,12 +36,9 @@ def calculate_rpn(sod): :raise: OutOfRangeError if one of the inputs falls outside the range [1, 10]. """ - if not 0 < sod["rpn_severity"] < 11: - raise OutOfRangeError(("RPN severity is outside the range [1, 10].")) - if not 0 < sod["rpn_occurrence"] < 11: - raise OutOfRangeError(("RPN occurrence is outside the range [1, 10].")) - if not 0 < sod["rpn_detection"] < 11: - raise OutOfRangeError(("RPN detection is outside the range [1, 10].")) + _do_validate_range(sod["rpn_severity"], 1, 10, "RPN severity") + _do_validate_range(sod["rpn_occurrence"], 1, 10, "RPN occurrence") + _do_validate_range(sod["rpn_detection"], 1, 10, "RPN detection") return ( int(sod["rpn_severity"]) @@ -47,7 +47,7 @@ def calculate_rpn(sod): ) -def calculate_mode_hazard_rate(item_hr, mode_ratio): +def calculate_mode_hazard_rate(item_hr: float, mode_ratio: float) -> float: """Calculate the failure mode hazard rate. >>> item_hr=0.000617 @@ -63,26 +63,15 @@ def calculate_mode_hazard_rate(item_hr, mode_ratio): :raise: OutOfRangeError if passed a negative item hazard rate or a mode ratio outside [0.0, 1.0]. """ - if item_hr < 0.0: - raise OutOfRangeError( - ( - "calculate_mode_hazard_rate() was passed a " - "negative value for the item hazard rate." - ) - ) - if not 0.0 <= mode_ratio <= 1.0: - raise OutOfRangeError( - ( - "calculate_mode_hazard_rate() was passed a " - "failure mode ratio outside the range of " - "[0.0, 1.0]." - ) - ) + _do_validate_range(item_hr, 0.0, float("inf"), "Item hazard rate") + _do_validate_range(mode_ratio, 0.0, 1.0, "Mode ratio") return item_hr * mode_ratio -def calculate_mode_criticality(mode_hr, mode_op_time, eff_prob): +def calculate_mode_criticality( + mode_hr: float, mode_op_time: float, eff_prob: float +) -> float: """Calculate the MIL-HDBK-1629A, Task 102 criticality. >>> mode_hr=0.00021595 @@ -99,21 +88,26 @@ def calculate_mode_criticality(mode_hr, mode_op_time, eff_prob): :raise: OutOfRangeError if passed a negative mode operating time or an effect probability outside [0.0, 1.0]. """ - if mode_op_time < 0.0: - raise OutOfRangeError( - ( - "calculate_mode_criticality() was passed a " - "negative value for failure mode operating " - "time." - ) - ) - if not 0.0 <= eff_prob <= 1.0: - raise OutOfRangeError( - ( - "calculate_mode_criticality() was passed a " - "failure effect probability outside the range " - "of [0.0, 1.0]." - ) - ) + _do_validate_range(mode_op_time, 0.0, float("inf"), "Mode operating time") + _do_validate_range(eff_prob, 0.0, 1.0, "Effect probability") return mode_hr * mode_op_time * eff_prob + + +def _do_validate_range( + value: Union[int, float], min_val: float, max_val: float, name: str +) -> None: + """Validate that a value is within a specified range. + + :param value: The value to validate. + :type value: int or float + :param min_val: The minimum allowable value (inclusive). + :type min_val: float + :param max_val: The maximum allowable value (inclusive). + :type max_val: float + :param name: The name of the value being validated (for error messages). + :type name: str + :raises OutOfRangeError: If the value is outside the specified range. + """ + if not min_val <= value <= max_val: + raise OutOfRangeError(f"{name} is outside the range [{min_val}, {max_val}].") diff --git a/tests/analyses/test_criticality.py b/tests/analyses/test_criticality.py index 851908800..18866cd11 100644 --- a/tests/analyses/test_criticality.py +++ b/tests/analyses/test_criticality.py @@ -21,12 +21,26 @@ @pytest.mark.unit @pytest.mark.calculation def test_calculate_rpn(): - """calculate_rpn() should return the product of the three input values on success.""" + """calculate_rpn() should return the product of the three input values on + success.""" _rpn = criticality.calculate_rpn(SOD) assert _rpn == 280 +@pytest.mark.unit +@pytest.mark.calculation +def test_calculate_rpn_boundary_values(): + """calculate_rpn() should handle boundary values correctly.""" + SOD = {"rpn_severity": 1, "rpn_occurrence": 1, "rpn_detection": 1} + _rpn = criticality.calculate_rpn(SOD) + assert _rpn == 1 + + SOD = {"rpn_severity": 10, "rpn_occurrence": 10, "rpn_detection": 10} + _rpn = criticality.calculate_rpn(SOD) + assert _rpn == 1000 + + @pytest.mark.unit @pytest.mark.calculation def test_calculate_rpn_out_of_range_severity_inputs(): @@ -65,86 +79,110 @@ def test_calculate_rpn_out_of_range_severity_inputs(): SOD["rpn_detection"] = 7 +@pytest.mark.unit +@pytest.mark.calculation +def test_calculate_rpn_invalid_types(): + """calculate_rpn() should raise TypeError when passed non-integer values.""" + with pytest.raises(TypeError): + SOD = {"rpn_severity": "high", "rpn_occurrence": 8, "rpn_detection": 7} + criticality.calculate_rpn(SOD) + + with pytest.raises(TypeError): + SOD = {"rpn_severity": 5, "rpn_occurrence": None, "rpn_detection": 7} + criticality.calculate_rpn(SOD) + + @pytest.mark.unit @pytest.mark.calculation def test_calculate_mode_hazard_rate(): - """calculate_mode_hazard_rate() should return the product of the item hazard rate and the mode ratio on success.""" + """calculate_mode_hazard_rate() should return the product of the item hazard rate + and the mode ratio on success.""" _mode_hr = criticality.calculate_mode_hazard_rate(0.000617, 0.35) - assert _mode_hr == 0.00021595 + assert _mode_hr == pytest.approx(0.00021595) + + +@pytest.mark.unit +@pytest.mark.calculation +def test_calculate_mode_hazard_rate_boundary_values(): + """calculate_mode_hazard_rate() should handle boundary values correctly.""" + assert criticality.calculate_mode_hazard_rate(0.000617, 0.0) == pytest.approx(0.0) + assert criticality.calculate_mode_hazard_rate(0.000617, 1.0) == pytest.approx( + 0.000617 + ) @pytest.mark.unit @pytest.mark.calculation def test_calculate_mode_hazard_rate_out_of_range_mode_ratio(): - """calculate_mode_hazard_rate() should raise an OutOfRangeError if the mode ratio is outside [0.0, 1.0].""" + """calculate_mode_hazard_rate() should raise an OutOfRangeError if the mode ratio is + outside [0.0, 1.0].""" with pytest.raises(OutOfRangeError) as e: criticality.calculate_mode_hazard_rate(0.000617, -0.35) - assert e.value.args[0] == ( - "calculate_mode_hazard_rate() was passed a " - "failure mode ratio outside the range of " - "[0.0, 1.0]." - ) + assert e.value.args[0] == ("Mode ratio is outside the range [0.0, 1.0].") with pytest.raises(OutOfRangeError) as e: criticality.calculate_mode_hazard_rate(0.000617, 1.35) - assert e.value.args[0] == ( - "calculate_mode_hazard_rate() was passed a " - "failure mode ratio outside the range of " - "[0.0, 1.0]." - ) + assert e.value.args[0] == ("Mode ratio is outside the range [0.0, 1.0].") @pytest.mark.unit @pytest.mark.calculation def test_calculate_mode_hazard_rate_out_of_range_item_hr(): - """calculate_mode_hazard_rate() should raise an OutOfRangeError if the item hazard rate is negative.""" + """calculate_mode_hazard_rate() should raise an OutOfRangeError if the item hazard + rate is negative.""" with pytest.raises(OutOfRangeError) as e: criticality.calculate_mode_hazard_rate(-0.000617, 0.35) - assert e.value.args[0] == ( - "calculate_mode_hazard_rate() was passed a " - "negative value for the item hazard rate." - ) + assert e.value.args[0] == ("Item hazard rate is outside the range [0.0, inf].") + + +@pytest.mark.unit +@pytest.mark.calculation +def test_calculate_mode_hazard_rate_zero_item_hr(): + """calculate_mode_hazard_rate() should return 0 when item hazard rate is 0.""" + _mode_hr = criticality.calculate_mode_hazard_rate(0.0, 0.35) + assert _mode_hr == pytest.approx(0.0) @pytest.mark.unit @pytest.mark.calculation def test_calculate_mode_criticality(): - """calculate_mode_criticality() should return the product of the mode hazard rate, mode operating time, and effect probability on success.""" + """calculate_mode_criticality() should return the product of the mode hazard rate, + mode operating time, and effect probability on success.""" _mode_crit = criticality.calculate_mode_criticality(0.00021595, 5.28, 0.75) - assert _mode_crit == 0.000855162 + assert _mode_crit == pytest.approx(0.000855162) + + +@pytest.mark.unit +@pytest.mark.calculation +def test_calculate_mode_criticality_boundary_values(): + """calculate_mode_criticality() should handle boundary values correctly.""" + assert criticality.calculate_mode_criticality(0.0, 5.28, 0.75) == pytest.approx(0.0) + assert criticality.calculate_mode_criticality( + float("inf"), 5.28, 0.75 + ) == pytest.approx(float("inf")) @pytest.mark.unit @pytest.mark.calculation def test_calculate_mode_criticality_out_of_range_op_time(): - """calculate_mode_criticality() should raise an OutOfRangeError when passed a negative value for operating time.""" + """calculate_mode_criticality() should raise an OutOfRangeError when passed a + negative value for operating time.""" with pytest.raises(OutOfRangeError) as e: criticality.calculate_mode_criticality(0.00021595, -5.28, 0.75) - assert e.value.args[0] == ( - "calculate_mode_criticality() was passed a " - "negative value for failure mode operating " - "time." - ) + assert e.value.args[0] == ("Mode operating time is outside the range [0.0, inf].") @pytest.mark.unit @pytest.mark.calculation def test_calculate_mode_criticality_out_of_range_eff_prob(): - """calculate_mode_criticality() should raise an OutOfRangeError when passed an effect probability outside the range [0.0, 1.0].""" + """calculate_mode_criticality() should raise an OutOfRangeError when passed an + effect probability outside the range [0.0, 1.0].""" with pytest.raises(OutOfRangeError) as e: criticality.calculate_mode_criticality(0.00021595, 5.28, -0.75) - assert e.value.args[0] == ( - "calculate_mode_criticality() was passed a " - "failure effect probability outside the range " - "of [0.0, 1.0]." - ) + assert e.value.args[0] == ("Effect probability is outside the range [0.0, 1.0].") with pytest.raises(OutOfRangeError) as e: criticality.calculate_mode_criticality(0.00021595, 5.28, 1.75) - assert e.value.args[0] == ( - "calculate_mode_criticality() was passed a " - "failure effect probability outside the range " - "of [0.0, 1.0]." - ) + assert e.value.args[0] == ("Effect probability is outside the range [0.0, 1.0].")