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