Implement limit checker
This commit is contained in:
@@ -5,6 +5,16 @@ and runtime context management for DVT characterisation tests.
|
||||
"""
|
||||
|
||||
from py_dvt_ate.framework.context import ITest, TestContext
|
||||
from py_dvt_ate.framework.limits import Limit, LimitSet, check_value, evaluate_results
|
||||
from py_dvt_ate.framework.logger import ITestLogger, TestLogger
|
||||
|
||||
__all__ = ["ITest", "ITestLogger", "TestContext", "TestLogger"]
|
||||
__all__ = [
|
||||
"ITest",
|
||||
"ITestLogger",
|
||||
"Limit",
|
||||
"LimitSet",
|
||||
"TestContext",
|
||||
"TestLogger",
|
||||
"check_value",
|
||||
"evaluate_results",
|
||||
]
|
||||
|
||||
238
src/py_dvt_ate/framework/limits.py
Normal file
238
src/py_dvt_ate/framework/limits.py
Normal file
@@ -0,0 +1,238 @@
|
||||
"""Limit checking utilities for test result evaluation.
|
||||
|
||||
This module provides utilities for evaluating measurements against specification
|
||||
limits and determining pass/fail status. Used by tests to check if results meet
|
||||
requirements and by the test runner to determine overall test status.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from py_dvt_ate.data.models import TestResult, TestStatus
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Limit:
|
||||
"""Specification limit for a parameter.
|
||||
|
||||
Represents a single limit specification with optional lower and upper bounds.
|
||||
Used to define test specifications and evaluate pass/fail.
|
||||
|
||||
Attributes:
|
||||
parameter: Parameter name this limit applies to.
|
||||
lower: Optional lower limit (inclusive). None means no lower limit.
|
||||
upper: Optional upper limit (inclusive). None means no upper limit.
|
||||
unit: Unit of measurement for the limits.
|
||||
|
||||
Example:
|
||||
temp_co_limit = Limit("temp_co", lower=-50.0, upper=50.0, unit="ppm/°C")
|
||||
"""
|
||||
|
||||
parameter: str
|
||||
lower: float | None = None
|
||||
upper: float | None = None
|
||||
unit: str = ""
|
||||
|
||||
def check(self, value: float) -> bool | None:
|
||||
"""Check if a value is within this limit.
|
||||
|
||||
Args:
|
||||
value: Value to check against limits.
|
||||
|
||||
Returns:
|
||||
True if value is within limits, False if outside limits.
|
||||
None if no limits are defined (informational parameter).
|
||||
|
||||
Example:
|
||||
limit = Limit("v_out", lower=3.25, upper=3.35, unit="V")
|
||||
limit.check(3.30) # Returns True
|
||||
limit.check(3.40) # Returns False
|
||||
"""
|
||||
if self.lower is None and self.upper is None:
|
||||
return None
|
||||
|
||||
lower_ok = self.lower is None or value >= self.lower
|
||||
upper_ok = self.upper is None or value <= self.upper
|
||||
return lower_ok and upper_ok
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LimitSet:
|
||||
"""Collection of limits for a test.
|
||||
|
||||
Groups multiple parameter limits together as a test specification.
|
||||
Can be loaded from configuration or defined programmatically.
|
||||
|
||||
Attributes:
|
||||
name: Name of this limit set (e.g., "nominal", "extended").
|
||||
limits: Dictionary mapping parameter names to Limit objects.
|
||||
|
||||
Example:
|
||||
limits = LimitSet(
|
||||
name="nominal",
|
||||
limits={
|
||||
"temp_co": Limit("temp_co", -50.0, 50.0, "ppm/°C"),
|
||||
"v_out": Limit("v_out", 3.25, 3.35, "V"),
|
||||
}
|
||||
)
|
||||
"""
|
||||
|
||||
name: str
|
||||
limits: dict[str, Limit]
|
||||
|
||||
def get_limit(self, parameter: str) -> Limit | None:
|
||||
"""Get the limit for a specific parameter.
|
||||
|
||||
Args:
|
||||
parameter: Parameter name to look up.
|
||||
|
||||
Returns:
|
||||
Limit object if found, None if parameter has no limit defined.
|
||||
"""
|
||||
return self.limits.get(parameter)
|
||||
|
||||
def check(self, parameter: str, value: float) -> bool | None:
|
||||
"""Check if a value is within limits for a parameter.
|
||||
|
||||
Args:
|
||||
parameter: Parameter name.
|
||||
value: Value to check.
|
||||
|
||||
Returns:
|
||||
True if within limits, False if outside limits.
|
||||
None if parameter has no limit defined.
|
||||
"""
|
||||
limit = self.get_limit(parameter)
|
||||
if limit is None:
|
||||
return None
|
||||
return limit.check(value)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, name: str, limits_dict: dict[str, Any]) -> "LimitSet":
|
||||
"""Create a LimitSet from a dictionary.
|
||||
|
||||
Useful for loading limit sets from YAML configuration files.
|
||||
|
||||
Args:
|
||||
name: Name for this limit set.
|
||||
limits_dict: Dictionary with parameter names as keys and limit
|
||||
specifications as values. Each limit spec should have:
|
||||
- "lower": Optional lower limit
|
||||
- "upper": Optional upper limit
|
||||
- "unit": Unit of measurement
|
||||
|
||||
Returns:
|
||||
LimitSet instance.
|
||||
|
||||
Example:
|
||||
config = {
|
||||
"temp_co": {"lower": -50.0, "upper": 50.0, "unit": "ppm/°C"},
|
||||
"v_out": {"lower": 3.25, "upper": 3.35, "unit": "V"},
|
||||
}
|
||||
limits = LimitSet.from_dict("nominal", config)
|
||||
"""
|
||||
limits = {}
|
||||
for param, spec in limits_dict.items():
|
||||
limits[param] = Limit(
|
||||
parameter=param,
|
||||
lower=spec.get("lower"),
|
||||
upper=spec.get("upper"),
|
||||
unit=spec.get("unit", ""),
|
||||
)
|
||||
return cls(name=name, limits=limits)
|
||||
|
||||
|
||||
def check_value(
|
||||
value: float,
|
||||
lower: float | None = None,
|
||||
upper: float | None = None,
|
||||
) -> bool | None:
|
||||
"""Check if a value is within specified limits.
|
||||
|
||||
Utility function for quick limit checking without creating Limit objects.
|
||||
|
||||
Args:
|
||||
value: Value to check.
|
||||
lower: Optional lower limit (inclusive).
|
||||
upper: Optional upper limit (inclusive).
|
||||
|
||||
Returns:
|
||||
True if value is within limits, False if outside limits.
|
||||
None if no limits are specified.
|
||||
|
||||
Example:
|
||||
check_value(3.30, lower=3.25, upper=3.35) # Returns True
|
||||
check_value(3.40, lower=3.25, upper=3.35) # Returns False
|
||||
check_value(3.30) # Returns None (no limits)
|
||||
"""
|
||||
if lower is None and upper is None:
|
||||
return None
|
||||
|
||||
lower_ok = lower is None or value >= lower
|
||||
upper_ok = upper is None or value <= upper
|
||||
return lower_ok and upper_ok
|
||||
|
||||
|
||||
def evaluate_results(results: list[TestResult]) -> TestStatus:
|
||||
"""Evaluate a list of test results to determine overall status.
|
||||
|
||||
Aggregates multiple test results into a single pass/fail determination.
|
||||
If any result fails its limits, the overall status is FAILED.
|
||||
If all results pass (or have no limits), the overall status is PASSED.
|
||||
|
||||
Args:
|
||||
results: List of TestResult objects to evaluate.
|
||||
|
||||
Returns:
|
||||
TestStatus.PASSED if all results pass their limits.
|
||||
TestStatus.FAILED if any result fails its limits.
|
||||
TestStatus.PASSED if no results have limits defined (informational only).
|
||||
|
||||
Example:
|
||||
results = [
|
||||
TestResult(..., value=25.0, lower_limit=-50.0, upper_limit=50.0),
|
||||
TestResult(..., value=3.30, lower_limit=3.25, upper_limit=3.35),
|
||||
]
|
||||
status = evaluate_results(results) # Returns TestStatus.PASSED
|
||||
"""
|
||||
if not results:
|
||||
return TestStatus.PASSED
|
||||
|
||||
# Check if any result failed
|
||||
for result in results:
|
||||
if result.passed is False:
|
||||
return TestStatus.FAILED
|
||||
|
||||
# All results passed (or had no limits)
|
||||
return TestStatus.PASSED
|
||||
|
||||
|
||||
def format_limit_violation(result: TestResult) -> str:
|
||||
"""Format a limit violation message for a failed result.
|
||||
|
||||
Creates a human-readable message describing why a result failed.
|
||||
Useful for logging and reporting.
|
||||
|
||||
Args:
|
||||
result: TestResult that failed its limits.
|
||||
|
||||
Returns:
|
||||
Formatted violation message.
|
||||
|
||||
Example:
|
||||
result = TestResult(..., parameter="v_out", value=3.40,
|
||||
lower_limit=3.25, upper_limit=3.35, unit="V")
|
||||
message = format_limit_violation(result)
|
||||
# Returns: "v_out: 3.400 V [FAIL] (limits: 3.250 to 3.350 V)"
|
||||
"""
|
||||
status = "PASS" if result.passed else "FAIL"
|
||||
limits_str = ""
|
||||
|
||||
if result.lower_limit is not None and result.upper_limit is not None:
|
||||
limits_str = f" (limits: {result.lower_limit:.3f} to {result.upper_limit:.3f} {result.unit})"
|
||||
elif result.lower_limit is not None:
|
||||
limits_str = f" (minimum: {result.lower_limit:.3f} {result.unit})"
|
||||
elif result.upper_limit is not None:
|
||||
limits_str = f" (maximum: {result.upper_limit:.3f} {result.unit})"
|
||||
|
||||
return f"{result.parameter}: {result.value:.3f} {result.unit} [{status}]{limits_str}"
|
||||
Reference in New Issue
Block a user