Implement limit checker

This commit is contained in:
2025-09-05 15:44:46 +00:00
parent 0816ba0122
commit 23aec13bc6
2 changed files with 249 additions and 1 deletions

View File

@@ -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.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 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",
]

View 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}"