From 9de1e172c4ff70ff3f055154a3e0b6e18f89965a Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Mon, 15 Sep 2025 18:32:58 +0000 Subject: [PATCH] Implement TempCo characterisation test --- src/py_dvt_ate/tests/thermal/tempco.py | 243 +++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 src/py_dvt_ate/tests/thermal/tempco.py diff --git a/src/py_dvt_ate/tests/thermal/tempco.py b/src/py_dvt_ate/tests/thermal/tempco.py new file mode 100644 index 0000000..7708aa2 --- /dev/null +++ b/src/py_dvt_ate/tests/thermal/tempco.py @@ -0,0 +1,243 @@ +"""Temperature Coefficient (TempCo) characterisation test. + +This test characterises the output voltage temperature coefficient by +sweeping the chamber temperature and measuring output voltage at each point. +The TempCo is calculated from the linear regression slope and expressed +in parts per million per degree Celsius (ppm/°C). +""" + +from py_dvt_ate.data.models import TestStatus +from py_dvt_ate.framework.context import TestContext +from py_dvt_ate.tests.base import BaseDVTTest + + +class TempCoTest(BaseDVTTest): + """Temperature coefficient characterisation test. + + Measures how output voltage varies with temperature. This is a critical + parameter for voltage regulators, as it indicates stability across + the operating temperature range. + + Test Procedure: + 1. Configure DUT supply voltage and load current + 2. Sweep chamber temperature from min to max + 3. At each temperature point: + - Wait for thermal stability + - Measure output voltage (averaged) + - Log measurement with conditions + 4. Calculate TempCo from linear regression + 5. Evaluate against specification limits + + Configuration: + temperatures: List of temperature points (°C). Default: [-40, -20, 0, 25, 50, 85] + input_voltage: DUT input voltage (V). Default: 5.0 + load_current: DUT load current (A). Default: 0.1 + settle_time: Additional settling time at each temp (s). Default: 5.0 + num_samples: Number of measurements to average per point. Default: 5 + tempco_limit: Maximum allowed TempCo magnitude (ppm/°C). Default: ±50.0 + """ + + @property + def name(self) -> str: + """Return test identifier.""" + return "tempco" + + @property + def description(self) -> str: + """Return test description.""" + return "Output voltage temperature coefficient" + + def execute(self, context: TestContext) -> TestStatus: + """Execute TempCo characterisation test. + + Args: + context: Test context with instruments, logger, and configuration. + + Returns: + PASSED if TempCo is within limits, FAILED otherwise. + ERROR if a critical failure occurs. + """ + try: + # Get configuration + config = context.config + temperatures = config.get("temperatures", [-40.0, -20.0, 0.0, 25.0, 50.0, 85.0]) + input_voltage = config.get("input_voltage", 5.0) + load_current = config.get("load_current", 0.1) + settle_time = config.get("settle_time", 5.0) + num_samples = config.get("num_samples", 5) + tempco_limit = config.get("tempco_limit", 50.0) + + context.logger.log_event( + f"Starting TempCo test: {len(temperatures)} temperature points, " + f"Vin={input_voltage}V, Iload={load_current}A", + level="INFO", + ) + + # Configure DUT power + context.logger.log_event( + f"Configuring PSU: Vin={input_voltage}V, Ilimit={load_current + 0.5}A", + level="INFO", + ) + psu = context.instruments.psu + psu.set_voltage(1, input_voltage) + psu.set_current_limit(1, load_current + 0.5) # Add headroom + psu.enable_output(1, True) + + # Storage for measurements + temp_points: list[float] = [] + vout_points: list[float] = [] + + # Temperature sweep + for temp_setpoint in temperatures: + context.logger.log_event( + f"Temperature point: {temp_setpoint}°C", + level="INFO", + ) + + # Wait for thermal stability + stable = self.wait_for_temperature( + context, + temp_setpoint, + timeout=300.0, + ) + if not stable: + context.logger.log_event( + f"Warning: Temperature did not stabilise at {temp_setpoint}°C", + level="WARNING", + ) + + # Additional settling for DUT junction temperature + self.thermal_settle(context, settle_time) + + # Measure output voltage (averaged) + actual_temp = context.instruments.chamber.get_temperature() + + def measure_vout() -> float: + return context.instruments.dmm.measure_dc_voltage() + + vout_mean, vout_std = self.measure_averaged( + measure_vout, + num_samples=num_samples, + ) + + # Log individual measurement + context.logger.log_measurement( + parameter="v_out", + value=vout_mean, + unit="V", + conditions={ + "temperature": actual_temp, + "input_voltage": input_voltage, + "load_current": load_current, + }, + ) + + context.logger.log_event( + f"Measured Vout = {vout_mean:.6f}V ± {vout_std * 1e6:.1f}μV " + f"at T={actual_temp:.2f}°C", + level="INFO", + ) + + # Store for TempCo calculation + temp_points.append(actual_temp) + vout_points.append(vout_mean) + + # Calculate TempCo from linear regression + tempco_ppm = self._calculate_tempco(temp_points, vout_points) + + context.logger.log_event( + f"Calculated TempCo = {tempco_ppm:.2f} ppm/°C", + level="INFO", + ) + + # Log result with limits + context.logger.log_result( + parameter="temp_co", + value=tempco_ppm, + unit="ppm/°C", + lower_limit=-abs(tempco_limit), + upper_limit=abs(tempco_limit), + ) + + # Evaluate pass/fail + passed = abs(tempco_ppm) <= tempco_limit + + if passed: + context.logger.log_event( + f"TempCo test PASSED: {tempco_ppm:.2f} ppm/°C within ±{tempco_limit} ppm/°C", + level="INFO", + ) + return TestStatus.PASSED + else: + context.logger.log_event( + f"TempCo test FAILED: {tempco_ppm:.2f} ppm/°C exceeds ±{tempco_limit} ppm/°C", + level="ERROR", + ) + return TestStatus.FAILED + + except Exception as e: + context.logger.log_event( + f"TempCo test ERROR: {e!s}", + level="ERROR", + ) + return TestStatus.ERROR + + finally: + # Cleanup: disable PSU output + try: + context.instruments.psu.enable_output(1, False) + context.logger.log_event("PSU output disabled", level="INFO") + except Exception: + pass # Best effort cleanup + + def _calculate_tempco( + self, + temperatures: list[float], + voltages: list[float], + ) -> float: + """Calculate temperature coefficient from measurements. + + Uses linear regression to find the slope (dV/dT), then converts + to ppm/°C relative to the nominal voltage (voltage at median temperature). + + Args: + temperatures: Temperature measurements in °C. + voltages: Output voltage measurements in V. + + Returns: + Temperature coefficient in ppm/°C. + + Raises: + ValueError: If insufficient data points. + """ + if len(temperatures) < 2 or len(temperatures) != len(voltages): + raise ValueError("Need at least 2 matching temperature-voltage pairs") + + n = len(temperatures) + + # Linear regression: V = a + b*T + # We want slope b = dV/dT + mean_t = sum(temperatures) / n + mean_v = sum(voltages) / n + + # Covariance and variance + cov = sum( + (t - mean_t) * (v - mean_v) + for t, v in zip(temperatures, voltages, strict=True) + ) + var_t = sum((t - mean_t) ** 2 for t in temperatures) + + if var_t == 0: + raise ValueError("Temperature variance is zero (all temps identical)") + + slope = cov / var_t # dV/dT in V/°C + + # Find nominal voltage (voltage at median temperature) + sorted_pairs = sorted(zip(temperatures, voltages, strict=True)) + mid_idx = len(sorted_pairs) // 2 + v_nominal = sorted_pairs[mid_idx][1] + + # Convert to ppm/°C: (dV/dT) / V_nom * 10^6 + tempco_ppm = (slope / v_nominal) * 1e6 + + return tempco_ppm