Implement TempCo characterisation test
This commit is contained in:
243
src/py_dvt_ate/tests/thermal/tempco.py
Normal file
243
src/py_dvt_ate/tests/thermal/tempco.py
Normal file
@@ -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
|
||||||
Reference in New Issue
Block a user