Source code for openlifu.plan.param_constraint
from __future__ import annotations
from dataclasses import dataclass
from typing import Annotated, Literal, Tuple
from openlifu.util.annotations import OpenLIFUFieldData
from openlifu.util.dict_conversion import DictMixin
PARAM_STATUS_SYMBOLS = {
"ok": "✅",
"warning": "❗",
"error": "❌"
}
[docs]
@dataclass
class ParameterConstraint(DictMixin):
operator: Annotated[Literal['<', '<=', '>', '>=', 'within', 'inside', 'outside', 'outside_inclusive'], OpenLIFUFieldData("Constraint operator", "Constraint operator used to evaluate parameter values")]
"""Constraint operator used to evaluate parameter values"""
warning_value: Annotated[float | int | Tuple[float | int, float | int] | None, OpenLIFUFieldData("Warning value", "Threshold or range that triggers a warning")] = None
"""Threshold or range that triggers a warning"""
error_value: Annotated[float | int | Tuple[float | int, float | int] | None, OpenLIFUFieldData("Error value", "Threshold or range that triggers an error")] = None
"""Threshold or range that triggers an error"""
def __post_init__(self):
if self.warning_value is None and self.error_value is None:
raise ValueError("At least one of warning_value or error_value must be set")
if self.operator in ['within', 'inside', 'outside', 'outside_inclusive']:
if self.warning_value and (not isinstance(self.warning_value, tuple) or len(self.warning_value) != 2 or self.warning_value[0] >= self.warning_value[1]):
raise ValueError("Warning value must be a sorted tuple of two numbers")
if self.error_value and (not isinstance(self.error_value, tuple) or len(self.error_value) != 2 or self.error_value[0] >= self.error_value[1]):
raise ValueError("Error value must be a sorted tuple of two numbers")
elif self.operator in ['<', '<=', '>', '>=']:
if self.warning_value is not None and not isinstance(self.warning_value, (int, float)):
raise ValueError("Warning value must be a single value")
if self.error_value is not None and not isinstance(self.error_value, (int, float)):
raise ValueError("Error value must be a single value")
@staticmethod
def compare(value, operator, threshold) -> bool:
if operator == '<':
return value < threshold
elif operator == '<=':
return value <= threshold
elif operator == '>':
return value > threshold
elif operator == '>=':
return value >= threshold
elif operator == 'within':
return threshold[0] < value < threshold[1]
elif operator == 'inside':
return threshold[0] <= value <= threshold[1]
elif operator == 'outside':
return value < threshold[0] or value > threshold[1]
elif operator == 'outside_inclusive':
return value <= threshold[0] or value >= threshold[1]
else:
raise ValueError(f"Unsupported operator: {operator}")
def is_warning(self, value: float | int) -> bool:
if self.warning_value is not None:
return not self.compare(value, self.operator, self.warning_value)
return False
def is_error(self, value: float | int) -> bool:
if self.error_value is not None:
return not self.compare(value, self.operator, self.error_value)
return False
def get_status(self, value: float) -> str:
if self.is_error(value):
return "error"
elif self.is_warning(value):
return "warning"
else:
return "ok"
def get_status_symbol(self, value: float) -> str:
return PARAM_STATUS_SYMBOLS[self.get_status(value)]