Skip to content

Commit

Permalink
refactor: criticality calculations (#1412)
Browse files Browse the repository at this point in the history
* refactor: criticality functions

* test: update and add tests to cover criticality function changes
  • Loading branch information
weibullguy authored Oct 11, 2024
1 parent e743ffb commit 7f01c55
Show file tree
Hide file tree
Showing 2 changed files with 110 additions and 78 deletions.
74 changes: 34 additions & 40 deletions src/ramstk/analyses/criticality.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@
# Copyright 2019 Doyle Rowland doyle.rowland <AT> reliaqual <DOT> 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
Expand All @@ -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"])
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}].")
114 changes: 76 additions & 38 deletions tests/analyses/test_criticality.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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].")

0 comments on commit 7f01c55

Please sign in to comment.