diff --git a/src/py_dvt_ate/tests/base.py b/src/py_dvt_ate/tests/base.py new file mode 100644 index 0000000..75e4ecb --- /dev/null +++ b/src/py_dvt_ate/tests/base.py @@ -0,0 +1,158 @@ +"""Base class and utilities for DVT test implementations. + +This module provides common functionality shared across all DVT tests, +including thermal settling helpers, measurement utilities, and statistical +calculations. +""" + +import time +from abc import ABC +from collections.abc import Callable + +from py_dvt_ate.framework.context import ITest, TestContext + + +class BaseDVTTest(ITest, ABC): + """Abstract base class for DVT tests with common utilities. + + Provides helper methods for thermal settling, measurement averaging, + and other common test patterns. All DVT tests should inherit from + this class rather than directly from ITest. + """ + + def wait_for_temperature( + self, + context: TestContext, + setpoint: float, + timeout: float = 300.0, + poll_interval: float = 1.0, + ) -> bool: + """Wait for thermal chamber to stabilise at setpoint. + + Sets the chamber temperature and waits until stable. Logs progress + to the test logger. + + Args: + context: Test context with instruments and logger. + setpoint: Target temperature in degrees Celsius. + timeout: Maximum wait time in seconds. Default 300s (5 minutes). + poll_interval: Time between stability checks. Default 1s. + + Returns: + True if temperature stabilised within timeout, False if timed out. + + Raises: + ConnectionError: If instrument communication fails. + IOError: If instrument reports error. + """ + chamber = context.instruments.chamber + + # Set the temperature + chamber.set_temperature(setpoint) + context.logger.log_event( + f"Set thermal chamber to {setpoint:.1f}°C, waiting for stability...", + level="INFO", + ) + + # Wait for stability + start_time = time.time() + elapsed = 0.0 + + while elapsed < timeout: + if chamber.is_stable(): + actual = chamber.get_temperature() + context.logger.log_event( + f"Chamber stable at {actual:.2f}°C " + f"(target {setpoint:.1f}°C) after {elapsed:.1f}s", + level="INFO", + ) + return True + + time.sleep(poll_interval) + elapsed = time.time() - start_time + + # Timeout + actual = chamber.get_temperature() + context.logger.log_event( + f"Timeout waiting for stability. Chamber at {actual:.2f}°C, " + f"target {setpoint:.1f}°C after {timeout:.1f}s", + level="WARNING", + ) + return False + + def measure_averaged( + self, + measurement_func: Callable[[], float], + num_samples: int = 5, + settle_time: float = 0.1, + ) -> tuple[float, float]: + """Take multiple measurements and return mean and standard deviation. + + Useful for reducing noise in measurements by averaging multiple samples. + + Args: + measurement_func: Function that returns a single measurement. + num_samples: Number of samples to average. Default 5. + settle_time: Delay between samples in seconds. Default 0.1s. + + Returns: + Tuple of (mean, standard_deviation). + + Raises: + ValueError: If num_samples < 1. + Exception: If measurement_func raises an exception. + """ + if num_samples < 1: + raise ValueError("num_samples must be at least 1") + + samples: list[float] = [] + for _ in range(num_samples): + if settle_time > 0 and len(samples) > 0: + time.sleep(settle_time) + samples.append(measurement_func()) + + mean = sum(samples) / len(samples) + + if len(samples) == 1: + std_dev = 0.0 + else: + variance = sum((x - mean) ** 2 for x in samples) / (len(samples) - 1) + std_dev = variance ** 0.5 + + return mean, std_dev + + def thermal_settle( + self, + context: TestContext, + additional_settle_time: float = 5.0, + ) -> None: + """Wait for additional thermal settling after chamber reports stable. + + After the chamber reports stable temperature, this adds additional + settling time to ensure the DUT junction temperature has also stabilised. + This is important for measurements sensitive to self-heating effects. + + Args: + context: Test context with logger. + additional_settle_time: Extra settling time in seconds. Default 5s. + """ + if additional_settle_time > 0: + context.logger.log_event( + f"Additional thermal settling for {additional_settle_time:.1f}s...", + level="INFO", + ) + time.sleep(additional_settle_time) + + def delay(self, seconds: float, message: str | None = None) -> None: + """Sleep for specified duration. + + Simple utility for adding delays in test sequences. + + Args: + seconds: Delay duration in seconds. + message: Optional message describing reason for delay. + """ + if message: + # Could log this if needed + pass + time.sleep(seconds)