Files
py-dvt-ate/docs/02_technical_specification.md
Kai Chappell 85024f8670 Restructure package for domain-driven design
Reorganise package structure to improve separation of concerns:
- instruments/ - SCPI, transport, drivers, interfaces, factory
- simulation/ - physics engine, virtual instruments, server
- framework/ - test runner, logger, limits, context
- tests/ - thermal/, electrical/ (DVT test implementations)
- data/ - repository, models
- reporting/ - generator, templates
- app/ - CLI, config, dashboard

This structure enables:
- Reusable instruments package for other test suites
- Clear separation of simulation (dev) vs production code
- Domain-focused package organisation

Updated documentation to reflect new paths.
2025-02-10 12:06:22 +00:00

1590 lines
59 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Technical Design Document
## py_dvt_ate: Implementation Specification
| Document ID | TDD-001 |
|-------------|---------|
| Version | 1.1.0 |
| Status | Draft |
| Author | Kai Chappell |
| Created | 2025-12-01 |
| Last Updated | 2025-12-01 |
---
## Purpose
This document specifies **how** to implement the py_dvt_ate system. It contains technical details including architecture diagrams, code structures, interfaces, schemas, and specifications.
For **what** the system must do, see `01_requirements.md`.
For **why** decisions were made, see `03_architecture_decisions.md`.
---
## Related Documents
| Document | Purpose |
|----------|---------|
| `01_requirements.md` | Defines **what** the system must do |
| `02_technical_specification.md` | Specifies **how** to implement (this document) |
| `03_architecture_decisions.md` | Explains **why** decisions were made |
---
## Table of Contents
1. [System Architecture](#1-system-architecture)
2. [Project Structure](#2-project-structure)
3. [Module Specifications](#3-module-specifications)
4. [Interface Definitions](#4-interface-definitions)
5. [SCPI Protocol Specification](#5-scpi-protocol-specification)
6. [Physics Model Specification](#6-physics-model-specification)
7. [Data Schemas](#7-data-schemas)
8. [Configuration Schema](#8-configuration-schema)
9. [API Specification](#9-api-specification)
10. [Development Phases](#10-development-phases)
---
## 1. System Architecture
### 1.1 System Context Diagram
```
┌─────────────────────────────────────────────────────────────────────────┐
│ SYSTEM BOUNDARY │
│ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Simulation Server │ │
│ │ (Separate Process) │ │
│ │ │ │
│ │ Physics Engine ◄───► Virtual Instruments │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ TCP/IP + SCPI │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ Test Application │ │
│ │ (Main Process) │ │
│ │ │ │
│ │ CLI / API ───► Test Executive ───► HAL ───► Drivers │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Data Persistence │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────┐
│ External Actors │
│ │
│ • DVT Engineer (CLI/API) │
│ • File System (Data) │
│ • Real Instruments (Future)│
└─────────────────────────────┘
```
### 1.2 Layer Architecture
```
┌─────────────────────────────────────────────────────────────────────────┐
│ PRESENTATION LAYER │
│ │
│ Components: CLI (Typer), REST API (FastAPI), Dashboard (Streamlit) │
│ Depends on: Application Layer │
├─────────────────────────────────────────────────────────────────────────┤
│ APPLICATION LAYER │
│ │
│ Components: Test Executive, Sequencer, Reporter │
│ Depends on: Domain Layer │
├─────────────────────────────────────────────────────────────────────────┤
│ DOMAIN LAYER │
│ │
│ Components: Test Definitions, Measurement Models, Limit Checking │
│ Depends on: HAL Interfaces (abstractions only) │
├─────────────────────────────────────────────────────────────────────────┤
│ HARDWARE ABSTRACTION LAYER │
│ │
│ Components: IThermalChamber, IPowerSupply, IMultimeter (Protocols) │
│ Depends on: None (pure interfaces) │
├─────────────────────────────────────────────────────────────────────────┤
│ INFRASTRUCTURE LAYER │
│ │
│ Components: Drivers, Transport, Repository, File I/O │
│ Depends on: HAL Interfaces (implements them) │
└─────────────────────────────────────────────────────────────────────────┘
```
### 1.3 Component Diagram
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ TEST APPLICATION │
│ │
│ ┌──────────────┐ ┌──────────────────────────────────────────────────┐ │
│ │ CLI │────▶│ Test Executive │ │
│ │ (Typer) │ │ ┌────────────┐ ┌──────────┐ ┌───────────────┐ │ │
│ └──────────────┘ │ │ Sequencer │ │ Logger │ │ Limit Checker │ │ │
│ │ └────────────┘ └──────────┘ └───────────────┘ │ │
│ ┌──────────────┐ └──────────────────────┬───────────────────────────┘ │
│ │ Streamlit │ │ │
│ │ Dashboard │────────────────────────────┤ │
│ └──────────────┘ ▼ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ Hardware Abstraction Layer │ │
│ │ │ │
│ │ ┌────────────────┐ ┌────────────────┐ │ │
│ │ │ IThermalChamber│ │ IPowerSupply │ ... │ │
│ │ └───────┬────────┘ └───────┬────────┘ │ │
│ └──────────┼──────────────────┼────────────────────┘ │
│ │ │ │
│ ┌──────────▼──────────────────▼────────────────────┐ │
│ │ Driver Layer │ │
│ │ ┌──────────────┐ ┌──────────────┐ │ │
│ │ │ChamberDriver │ │ PSU Driver │ ... │ │
│ │ └──────┬───────┘ └──────┬───────┘ │ │
│ └─────────┼────────────────┼───────────────────────┘ │
│ │ │ │
│ ┌─────────▼────────────────▼───────────────────────┐ │
│ │ Transport Layer │ │
│ │ (SCPI over TCP/IP) │ │
│ └─────────────────────┬────────────────────────────┘ │
│ │ │
└─────────────────────────────────────────────┼────────────────────────────────┘
│ TCP/IP
┌─────────────────────────────────────────────────────────────────────────────┐
│ SIMULATION SERVER │
│ │
│ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ Instrument Servers │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │
│ │ │ Thermal │ │ Power │ │ DMM │ │ │
│ │ │ Chamber │ │ Supply │ │ Server │ │ │
│ │ │ :5001 │ │ :5002 │ │ :5003 │ │ │
│ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │
│ │ │ │ │ │ │
│ │ └─────────────────┼─────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────┐ │ │
│ │ │ Coupled Physics Engine │ │ │
│ │ │ ┌─────────────────────────┐ │ │ │
│ │ │ │ DUT Thermal Model │ │ │ │
│ │ │ │ DUT Electrical Model │ │ │ │
│ │ │ │ Environment Model │ │ │ │
│ │ │ └─────────────────────────┘ │ │ │
│ │ └─────────────────────────────┘ │ │
│ └────────────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 1.4 Deployment Diagram
```
┌─────────────────────────────────────────────────────────────────┐
│ Docker Compose │
│ │
│ ┌────────────────────────┐ ┌────────────────────────────┐ │
│ │ simulation-server │ │ test-application │ │
│ │ │ │ │ │
│ │ - Physics Engine │ │ - CLI │ │
│ │ - Instrument Servers │ │ - REST API │ │
│ │ - DUT Models │ │ - Test Executive │ │
│ │ │ │ - Streamlit Dashboard │ │
│ │ Ports: 5001-5003 │ │ Ports: 8000, 8501 │ │
│ └────────────────────────┘ └────────────────────────────┘ │
│ │ │ │
│ └───────────────────────────┘ │
│ Internal Network │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Volumes │ │
│ │ ./data/results.db ./data/measurements/ │ │
│ │ ./config/ ./reports/ │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
---
## 2. Project Structure
### 2.1 Directory Layout
```
py_dvt_ate/
├── pyproject.toml # Project metadata and dependencies
├── README.md # Project overview and quick start
├── CHANGELOG.md # Version history
├── docs/
│ ├── 01_requirements.md # Business Requirements
│ ├── 02_technical_specification.md # Technical Design (this doc)
│ ├── 03_architecture_decisions.md # Architecture Decisions
│ └── 04_development_plan.md # Sprint breakdown
├── src/py_dvt_ate/
│ ├── __init__.py # Package version
│ ├── py.typed # PEP 561 marker
│ │
│ ├── instruments/ # INSTRUMENT CONTROL (reusable)
│ │ ├── __init__.py
│ │ ├── interfaces.py # IThermalChamber, IPowerSupply, IMultimeter
│ │ ├── scpi.py # SCPI parser (shared protocol)
│ │ ├── factory.py # Creates instrument sets from config
│ │ ├── transport/ # Connection layer
│ │ │ ├── __init__.py
│ │ │ ├── base.py # Transport protocol
│ │ │ ├── tcp.py # TCP socket transport
│ │ │ └── visa.py # PyVISA transport (future)
│ │ └── drivers/ # SCPI driver implementations
│ │ ├── __init__.py
│ │ ├── base.py # Base driver
│ │ ├── chamber.py # Thermal chamber driver
│ │ ├── power_supply.py # PSU driver
│ │ └── multimeter.py # DMM driver
│ │
│ ├── simulation/ # PHYSICS SIMULATION (dev/test only)
│ │ ├── __init__.py
│ │ ├── server.py # TCP server hosting virtual instruments
│ │ ├── physics/ # Physics engine
│ │ │ ├── __init__.py
│ │ │ ├── engine.py # Main simulation loop
│ │ │ ├── thermal.py # Thermal calculations
│ │ │ └── models/ # DUT models
│ │ │ ├── __init__.py
│ │ │ ├── base.py # DUT protocol
│ │ │ └── ldo.py # LDO model
│ │ └── virtual/ # Virtual instrument implementations
│ │ ├── __init__.py
│ │ ├── base.py # Base virtual instrument
│ │ ├── chamber.py # Virtual thermal chamber
│ │ ├── power_supply.py # Virtual PSU
│ │ └── multimeter.py # Virtual DMM
│ │
│ ├── framework/ # TEST FRAMEWORK (reusable)
│ │ ├── __init__.py
│ │ ├── runner.py # Test sequencer
│ │ ├── context.py # Runtime context
│ │ ├── logger.py # Measurement logging
│ │ ├── limits.py # Pass/fail evaluation
│ │ └── models.py # Framework models
│ │
│ ├── tests/ # DVT TEST IMPLEMENTATIONS
│ │ ├── __init__.py
│ │ ├── base.py # Base test class
│ │ ├── thermal/ # Thermal characterisation tests
│ │ │ ├── __init__.py
│ │ │ └── tempco.py # Temperature coefficient test
│ │ └── electrical/ # Electrical characterisation tests
│ │ ├── __init__.py
│ │ └── load_regulation.py # Load regulation test
│ │
│ ├── data/ # DATA PERSISTENCE (shared)
│ │ ├── __init__.py
│ │ ├── repository.py # Data access layer
│ │ └── models.py # Data models
│ │
│ ├── reporting/ # REPORT GENERATION (standalone)
│ │ ├── __init__.py
│ │ ├── generator.py # Report generator
│ │ └── templates/ # Report templates
│ │
│ └── app/ # APPLICATION ENTRY POINTS
│ ├── __init__.py
│ ├── cli.py # Command-line interface
│ ├── config.py # YAML loading
│ └── dashboard/ # Streamlit dashboard
│ ├── __init__.py
│ └── app.py # Main Streamlit app
├── tests/ # pytest test suite
│ ├── conftest.py # pytest fixtures
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests
├── config/ # Configuration files
│ └── default.yaml # Default configuration
└── docker/ # Docker deployment
├── Dockerfile.server # Simulation server image
├── Dockerfile.app # Test application image
└── docker-compose.yml # Full stack orchestration
```
### 2.2 Package Dependencies
```
Dependency Graph:
app/ ──────────────▶ framework/ ──────────────▶ instruments/
│ │ │
│ ▼ │
│ data/ ◀────────────────────────┘
│ ▲
▼ │
reporting/ ──────────────┘
simulation/ ─────────────────────────────────▶ instruments/
Key:
- app/ : CLI, dashboard, config loading (PRESENTATION)
- framework/ : Test runner, logger, limits (APPLICATION)
- instruments/ : Interfaces, drivers, transport, SCPI (DOMAIN)
- data/ : Persistence layer (INFRASTRUCTURE)
- reporting/ : Report generation (standalone)
- simulation/ : Physics engine, virtual instruments (DEVELOPMENT)
```
---
## 3. Module Specifications
### 3.1 Instruments Package
**Responsibility**: Everything about talking to lab instruments.
**Key Components**:
| Component | File | Purpose |
|-----------|------|---------|
| Interfaces | `instruments/interfaces.py` | IThermalChamber, IPowerSupply, IMultimeter protocols |
| SCPIParser | `instruments/scpi.py` | Parse SCPI command strings |
| Factory | `instruments/factory.py` | Create instrument sets from config |
| Transport | `instruments/transport/` | TCP, VISA connection layer |
| Drivers | `instruments/drivers/` | SCPI command implementations |
**Command Processing Flow**:
```
High-level call → Driver → SCPI command → Transport → Instrument
```
---
### 3.2 Simulation Package
**Responsibility**: Physics simulation for development without real hardware.
**Key Components**:
| Component | File | Purpose |
|-----------|------|---------|
| Server | `simulation/server.py` | TCP server hosting virtual instruments |
| PhysicsEngine | `simulation/physics/engine.py` | Main simulation loop |
| ThermalModel | `simulation/physics/thermal.py` | Heat transfer calculations |
| DUTBase | `simulation/physics/models/base.py` | Abstract DUT interface |
| LDOModel | `simulation/physics/models/ldo.py` | LDO voltage regulator model |
| VirtualChamber | `simulation/virtual/chamber.py` | Virtual thermal chamber |
| VirtualPSU | `simulation/virtual/power_supply.py` | Virtual power supply |
| VirtualDMM | `simulation/virtual/multimeter.py` | Virtual multimeter |
**State Management**:
- Engine maintains global simulation time
- State updates at fixed timestep (default 10ms = 100Hz)
- Immutable state snapshots returned to callers
---
### 3.3 Framework Package
**Responsibility**: Test execution infrastructure.
**Key Components**:
| Component | File | Purpose |
|-----------|------|---------|
| TestRunner | `framework/runner.py` | Sequences test steps |
| TestContext | `framework/context.py` | Runtime context |
| TestLogger | `framework/logger.py` | Measurement logging |
| LimitChecker | `framework/limits.py` | Pass/fail evaluation |
| Models | `framework/models.py` | TestStatus, TestResult, etc. |
---
### 3.4 Data Package
**Responsibility**: Data persistence for test results.
**Key Components**:
| Component | File | Purpose |
|-----------|------|---------|
| Repository | `data/repository.py` | Data access layer |
| Models | `data/models.py` | TestRun, Measurement dataclasses |
---
### 3.5 Reporting Package
**Responsibility**: Report generation from stored data.
**Key Components**:
| Component | File | Purpose |
|-----------|------|---------|
| Generator | `reporting/generator.py` | Creates reports from data |
| Templates | `reporting/templates/` | Report templates |
---
### 3.6 App Package
**Responsibility**: Application entry points.
**Key Components**:
| Component | File | Purpose |
|-----------|------|---------|
| CLI | `app/cli.py` | Command-line interface (Typer) |
| Config | `app/config.py` | YAML loading, instance creation |
| Dashboard | `app/dashboard/app.py` | Streamlit application |
---
## 4. Interface Definitions
### 4.1 Instrument Interfaces
```python
# py_dvt_ate/instruments/interfaces.py
from typing import Protocol, runtime_checkable
@runtime_checkable
class IThermalChamber(Protocol):
"""Hardware abstraction for thermal chambers."""
def set_temperature(self, setpoint: float) -> None:
"""Set target temperature in degrees Celsius."""
...
def get_temperature(self) -> float:
"""Get current actual temperature in degrees Celsius."""
...
def get_setpoint(self) -> float:
"""Get current temperature setpoint."""
...
def is_stable(self) -> bool:
"""Check if temperature has stabilised at setpoint."""
...
def wait_until_stable(
self,
timeout: float = 300.0,
poll_interval: float = 1.0
) -> bool:
"""
Block until temperature stabilises or timeout.
Returns:
True if stable, False if timeout
"""
...
def set_ramp_rate(self, rate: float) -> None:
"""Set temperature ramp rate in degrees C per minute."""
...
@runtime_checkable
class IPowerSupply(Protocol):
"""Hardware abstraction for programmable power supplies."""
def set_voltage(self, channel: int, voltage: float) -> None:
"""Set output voltage for specified channel."""
...
def get_voltage(self, channel: int) -> float:
"""Get voltage setpoint for specified channel."""
...
def set_current_limit(self, channel: int, current: float) -> None:
"""Set current limit for specified channel."""
...
def get_current_limit(self, channel: int) -> float:
"""Get current limit for specified channel."""
...
def measure_voltage(self, channel: int) -> float:
"""Measure actual output voltage."""
...
def measure_current(self, channel: int) -> float:
"""Measure actual output current."""
...
def enable_output(self, channel: int, enable: bool) -> None:
"""Enable or disable channel output."""
...
def is_output_enabled(self, channel: int) -> bool:
"""Check if channel output is enabled."""
...
@runtime_checkable
class IMultimeter(Protocol):
"""Hardware abstraction for digital multimeters."""
def measure_dc_voltage(self, range: str = "AUTO") -> float:
"""Measure DC voltage. Range: AUTO, 0.1, 1, 10, 100, 1000."""
...
def measure_dc_current(self, range: str = "AUTO") -> float:
"""Measure DC current."""
...
def measure_resistance(self, range: str = "AUTO") -> float:
"""Measure resistance."""
...
def set_integration_time(self, nplc: float) -> None:
"""Set integration time in power line cycles (0.1 to 100)."""
...
@runtime_checkable
class ITestLogger(Protocol):
"""Abstraction for test data logging."""
def log_measurement(
self,
parameter: str,
value: float,
unit: str,
conditions: dict[str, float] | None = None
) -> None:
"""Log a single measurement."""
...
def log_result(
self,
parameter: str,
value: float,
unit: str,
lower_limit: float | None = None,
upper_limit: float | None = None
) -> None:
"""Log a test result with optional limits."""
...
def log_event(self, message: str, level: str = "INFO") -> None:
"""Log a test event or message."""
...
```
### 4.2 Transport Interface
```python
# py_dvt_ate/instruments/transport/base.py
from typing import Protocol
class Transport(Protocol):
"""Abstract transport interface for instrument communication."""
def connect(self) -> None:
"""Establish connection to instrument."""
...
def disconnect(self) -> None:
"""Close connection to instrument."""
...
def write(self, command: str) -> None:
"""Send command to instrument."""
...
def read(self, timeout: float | None = None) -> str:
"""Read response from instrument."""
...
def query(self, command: str, timeout: float | None = None) -> str:
"""Send command and read response."""
...
@property
def is_connected(self) -> bool:
"""Check if connection is active."""
...
```
### 4.3 Test Interface
```python
# py_dvt_ate/framework/models.py
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Protocol
from uuid import UUID
class TestStatus(Enum):
PENDING = "pending"
RUNNING = "running"
PASSED = "passed"
FAILED = "failed"
ERROR = "error"
SKIPPED = "skipped"
@dataclass(frozen=True)
class Measurement:
"""Immutable measurement record."""
timestamp: datetime
parameter: str
value: float
unit: str
conditions: dict[str, float] = field(default_factory=dict)
@dataclass(frozen=True)
class TestResult:
"""Immutable test result with limits."""
parameter: str
value: float
unit: str
lower_limit: float | None = None
upper_limit: float | None = None
@property
def passed(self) -> bool | None:
"""Evaluate pass/fail. None if no limits defined."""
if self.lower_limit is None and self.upper_limit is None:
return None
lower_ok = self.lower_limit is None or self.value >= self.lower_limit
upper_ok = self.upper_limit is None or self.value <= self.upper_limit
return lower_ok and upper_ok
@dataclass
class TestContext:
"""Runtime context for test execution."""
run_id: UUID
instruments: "InstrumentSet"
logger: "ITestLogger"
config: dict
class ITest(Protocol):
"""Interface for test implementations."""
@property
def name(self) -> str:
"""Test name identifier."""
...
@property
def description(self) -> str:
"""Human-readable test description."""
...
def execute(self, context: TestContext) -> TestStatus:
"""Execute the test, return status."""
...
```
### 4.4 Factory Interface
```python
# py_dvt_ate/instruments/factory.py
from dataclasses import dataclass
from typing import Literal
from py_dvt_ate.instruments.interfaces import IThermalChamber, IPowerSupply, IMultimeter
@dataclass
class InstrumentSet:
"""Container for all instruments."""
chamber: IThermalChamber
psu: IPowerSupply
dmm: IMultimeter
@dataclass
class InstrumentConfig:
"""Configuration for instrument connections."""
backend: Literal["simulator", "pyvisa"]
# Simulator settings
simulator_host: str = "localhost"
chamber_port: int = 5001
psu_port: int = 5002
dmm_port: int = 5003
# PyVISA settings (for real hardware)
chamber_visa: str | None = None
psu_visa: str | None = None
dmm_visa: str | None = None
class InstrumentFactory:
"""Factory for creating instrument sets from configuration."""
@staticmethod
def create(config: InstrumentConfig) -> InstrumentSet:
"""Create instrument set based on configuration."""
if config.backend == "simulator":
return InstrumentFactory._create_simulated(config)
elif config.backend == "pyvisa":
return InstrumentFactory._create_pyvisa(config)
else:
raise ValueError(f"Unknown backend: {config.backend}")
@staticmethod
def _create_simulated(config: InstrumentConfig) -> InstrumentSet:
"""Create simulated instruments."""
from py_dvt_ate.instruments.transport.tcp import TCPTransport
from py_dvt_ate.instruments.drivers.chamber import ThermalChamberDriver
from py_dvt_ate.instruments.drivers.power_supply import PowerSupplyDriver
from py_dvt_ate.instruments.drivers.multimeter import MultimeterDriver
chamber_transport = TCPTransport(config.simulator_host, config.chamber_port)
psu_transport = TCPTransport(config.simulator_host, config.psu_port)
dmm_transport = TCPTransport(config.simulator_host, config.dmm_port)
return InstrumentSet(
chamber=ThermalChamberDriver(chamber_transport),
psu=PowerSupplyDriver(psu_transport),
dmm=MultimeterDriver(dmm_transport),
)
@staticmethod
def _create_pyvisa(config: InstrumentConfig) -> InstrumentSet:
"""Create PyVISA instruments for real hardware."""
# Implementation would use pyvisa.ResourceManager
raise NotImplementedError("PyVISA backend not yet implemented")
```
---
## 5. SCPI Protocol Specification
### 5.1 Common Commands (IEEE 488.2)
All instruments implement these standard commands:
| Command | Response | Description |
|---------|----------|-------------|
| `*IDN?` | `<manufacturer>,<model>,<serial>,<version>` | Identity query |
| `*RST` | - | Reset to default state |
| `*CLS` | - | Clear status registers |
| `*OPC?` | `1` | Operation complete query |
| `*OPC` | - | Set OPC bit when complete |
| `SYST:ERR?` | `<code>,"<message>"` | Query error queue |
### 5.2 Thermal Chamber Commands
| Command | Parameters | Response | Description |
|---------|------------|----------|-------------|
| `TEMP:SETPOINT` | `<float>` | - | Set target temperature (°C) |
| `TEMP:SETPOINT?` | - | `<float>` | Query temperature setpoint |
| `TEMP:ACTUAL?` | - | `<float>` | Query actual temperature |
| `TEMP:RAMP:RATE` | `<float>` | - | Set ramp rate (°C/min) |
| `TEMP:RAMP:RATE?` | - | `<float>` | Query ramp rate |
| `TEMP:STAB:WIN` | `<float>` | - | Set stability window (±°C) |
| `TEMP:STAB:TIME` | `<float>` | - | Set stability time (seconds) |
| `TEMP:STAB?` | - | `0` or `1` | Query stability status |
**Example Session**:
```
> *IDN?
< py_dvt_ate,VirtualChamber,SN001,1.0.0
> TEMP:SETPOINT 85.0
> TEMP:SETPOINT?
< 85.0
> TEMP:ACTUAL?
< 27.3
> TEMP:STAB?
< 0
```
### 5.3 Power Supply Commands
| Command | Parameters | Response | Description |
|---------|------------|----------|-------------|
| `INST:SEL` | `CH1` or `CH2` | - | Select active channel |
| `INST:SEL?` | - | `CH1` or `CH2` | Query selected channel |
| `VOLT` | `<float>` | - | Set voltage (selected channel) |
| `VOLT?` | - | `<float>` | Query voltage setpoint |
| `CURR` | `<float>` | - | Set current limit |
| `CURR?` | - | `<float>` | Query current limit |
| `MEAS:VOLT?` | - | `<float>` | Measure actual voltage |
| `MEAS:CURR?` | - | `<float>` | Measure actual current |
| `MEAS:POW?` | - | `<float>` | Measure power (V × I) |
| `OUTP` | `ON` or `OFF` or `0` or `1` | - | Enable/disable output |
| `OUTP?` | - | `0` or `1` | Query output state |
**Example Session**:
```
> *IDN?
< py_dvt_ate,VirtualPSU,SN002,1.0.0
> INST:SEL CH1
> VOLT 5.0
> CURR 0.5
> OUTP ON
> MEAS:VOLT?
< 4.998
> MEAS:CURR?
< 0.052
```
### 5.4 Multimeter Commands
| Command | Parameters | Response | Description |
|---------|------------|----------|-------------|
| `CONF:VOLT:DC` | `[<range>]` | - | Configure DC voltage measurement |
| `CONF:CURR:DC` | `[<range>]` | - | Configure DC current measurement |
| `CONF:RES` | `[<range>]` | - | Configure resistance measurement |
| `MEAS:VOLT:DC?` | `[<range>]` | `<float>` | Measure DC voltage |
| `MEAS:CURR:DC?` | `[<range>]` | `<float>` | Measure DC current |
| `MEAS:RES?` | `[<range>]` | `<float>` | Measure resistance |
| `READ?` | - | `<float>` | Read with current config |
| `SENS:VOLT:DC:NPLC` | `<float>` | - | Set integration time (PLCs) |
| `SENS:VOLT:DC:NPLC?` | - | `<float>` | Query integration time |
**Range Values**: `AUTO`, `0.1`, `1`, `10`, `100`, `1000`
**Example Session**:
```
> *IDN?
< py_dvt_ate,VirtualDMM,SN003,1.0.0
> CONF:VOLT:DC AUTO
> SENS:VOLT:DC:NPLC 10
> MEAS:VOLT:DC?
< 3.2987
```
### 5.5 SCPI Parser Specification
```python
# py_dvt_ate/instruments/scpi.py
from dataclasses import dataclass
@dataclass
class SCPICommand:
"""Parsed SCPI command."""
header: str # e.g., "TEMP:SETPOINT" or "*IDN"
arguments: list[str] # e.g., ["85.0"] or []
is_query: bool # True if ends with '?'
@property
def keyword(self) -> str:
"""Return the command keyword without '?'."""
return self.header.rstrip('?')
class SCPIParser:
"""Parse SCPI command strings."""
def parse(self, command_string: str) -> SCPICommand:
"""
Parse a SCPI command string.
Examples:
"*IDN?" -> SCPICommand("*IDN", [], True)
"VOLT 3.3" -> SCPICommand("VOLT", ["3.3"], False)
"TEMP:SETPOINT?" -> SCPICommand("TEMP:SETPOINT", [], True)
"""
command_string = command_string.strip()
is_query = command_string.endswith('?')
# Split into header and arguments
parts = command_string.split(None, 1) # Split on first whitespace
header = parts[0]
arguments = []
if len(parts) > 1:
# Parse comma-separated arguments
arg_string = parts[1]
arguments = [arg.strip() for arg in arg_string.split(',')]
return SCPICommand(
header=header,
arguments=arguments,
is_query=is_query
)
```
---
## 6. Physics Model Specification
### 6.1 Thermal Model
**State Variables**:
- `T_chamber`: Chamber air temperature (°C)
- `T_case`: DUT case temperature (°C)
- `T_junction`: DUT junction temperature (°C)
**Parameters**:
| Parameter | Symbol | Typical Value | Unit | Description |
|-----------|--------|---------------|------|-------------|
| Chamber time constant | τ_chamber | 30 | s | Thermal response time |
| Case time constant | τ_case | 5 | s | Package thermal response |
| Junction time constant | τ_junction | 0.5 | s | Die thermal response |
| Thermal resistance (junction-case) | θ_jc | 15 | °C/W | Junction to case |
| Thermal resistance (case-ambient) | θ_ca | 5 | °C/W | Case to ambient |
**Differential Equations**:
```
Chamber temperature (first-order response to setpoint):
dT_chamber/dt = (T_setpoint - T_chamber) / τ_chamber
Case temperature (driven by chamber and self-heating):
dT_case/dt = (T_chamber - T_case + P_diss × θ_ca) / τ_case
Junction temperature (instantaneous, no thermal mass at die level):
T_junction = T_case + P_diss × θ_jc
```
### 6.2 LDO Electrical Model
**State Variables**:
- `V_in`: Input voltage (V)
- `I_load`: Load current (A)
- `V_out`: Output voltage (V)
- `I_q`: Quiescent current (A)
**Temperature-Dependent Parameters**:
| Parameter | Formula | Description |
|-----------|---------|-------------|
| Output voltage | `V_out(T) = V_nom × (1 + TC_vout × (T - 25) × 1e-6)` | TC in ppm/°C |
| Quiescent current | `I_q(T) = I_q_25 × (1 + TC_iq × (T - 25))` | TC as ratio/°C |
| Dropout voltage | `V_do(T) = V_do_25 × (T / 300)^1.5` | Increases with temp |
**Power Dissipation**:
```
P_diss = (V_in - V_out) × I_load + V_in × I_q
```
**LDO Model Parameters**:
| Parameter | Symbol | Default Value | Unit |
|-----------|--------|---------------|------|
| Nominal output voltage | V_nom | 3.3 | V |
| Output voltage TempCo | TC_vout | 50 | ppm/°C |
| Quiescent current at 25°C | I_q_25 | 50 | µA |
| Quiescent current TempCo | TC_iq | 0.003 | 1/°C |
| Dropout voltage at 25°C | V_do_25 | 0.3 | V |
| Max output current | I_max | 0.5 | A |
### 6.3 Physics Engine Implementation
```python
# py_dvt_ate/simulation/physics/engine.py
from dataclasses import dataclass
@dataclass(frozen=True)
class ThermalState:
"""Immutable thermal state snapshot."""
chamber_temperature: float # °C
case_temperature: float # °C
junction_temperature: float # °C
timestamp: float # seconds since start
@dataclass(frozen=True)
class ElectricalState:
"""Immutable electrical state snapshot."""
input_voltage: float # V
output_voltage: float # V
load_current: float # A
quiescent_current: float # A
power_dissipation: float # W
class PhysicsEngine:
"""
Coupled thermal-electrical physics simulation.
Runs at fixed timestep, updating thermal and electrical state.
"""
def __init__(
self,
update_rate_hz: float = 100.0,
dut_model: "DUTModel" = None
):
self.dt = 1.0 / update_rate_hz
self.dut = dut_model or LDOModel()
# Thermal parameters
self.tau_chamber = 30.0 # seconds
self.tau_case = 5.0 # seconds
self.theta_jc = 15.0 # °C/W
self.theta_ca = 5.0 # °C/W
# State
self._t_setpoint = 25.0
self._t_chamber = 25.0
self._t_case = 25.0
self._v_in = 0.0
self._i_load = 0.0
self._output_enabled = False
self._sim_time = 0.0
def step(self) -> None:
"""Advance simulation by one timestep."""
# Update chamber temperature (first-order response)
dT_chamber = (self._t_setpoint - self._t_chamber) / self.tau_chamber
self._t_chamber += dT_chamber * self.dt
# Calculate power dissipation
if self._output_enabled:
p_diss = self._get_power_dissipation()
else:
p_diss = 0.0
# Update case temperature
dT_case = (self._t_chamber - self._t_case + p_diss * self.theta_ca) / self.tau_case
self._t_case += dT_case * self.dt
self._sim_time += self.dt
def get_thermal_state(self) -> ThermalState:
"""Get current thermal state."""
t_junction = self._t_case + self._get_power_dissipation() * self.theta_jc
return ThermalState(
chamber_temperature=self._t_chamber,
case_temperature=self._t_case,
junction_temperature=t_junction,
timestamp=self._sim_time
)
def get_electrical_state(self) -> ElectricalState:
"""Get current electrical state."""
t_junction = self._t_case + self._get_power_dissipation() * self.theta_jc
v_out = self.dut.calculate_output_voltage(t_junction) if self._output_enabled else 0.0
i_q = self.dut.calculate_quiescent_current(t_junction) if self._output_enabled else 0.0
p_diss = self._get_power_dissipation()
return ElectricalState(
input_voltage=self._v_in,
output_voltage=v_out,
load_current=self._i_load if self._output_enabled else 0.0,
quiescent_current=i_q,
power_dissipation=p_diss
)
def set_chamber_setpoint(self, temperature: float) -> None:
"""Set chamber target temperature."""
self._t_setpoint = temperature
def set_input_voltage(self, voltage: float) -> None:
"""Set DUT input voltage."""
self._v_in = voltage
def set_load_current(self, current: float) -> None:
"""Set DUT load current."""
self._i_load = current
def set_output_enabled(self, enabled: bool) -> None:
"""Enable or disable DUT power."""
self._output_enabled = enabled
def _get_power_dissipation(self) -> float:
"""Calculate current power dissipation."""
if not self._output_enabled:
return 0.0
t_junction = self._t_case # Approximate for calculation
return self.dut.calculate_power_dissipation(
self._v_in, self._i_load, t_junction
)
```
---
## 7. Data Schemas
### 7.1 SQLite Schema (Metadata)
```sql
-- File: data/migrations/001_initial.sql
-- Test run metadata
CREATE TABLE IF NOT EXISTS test_runs (
id TEXT PRIMARY KEY, -- UUID
test_name TEXT NOT NULL,
description TEXT,
started_at TEXT NOT NULL, -- ISO8601 timestamp
completed_at TEXT, -- ISO8601 timestamp
status TEXT NOT NULL DEFAULT 'pending', -- pending, running, passed, failed, error
config_json TEXT NOT NULL, -- Test configuration as JSON
operator TEXT,
notes TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
-- Scalar test results with limits
CREATE TABLE IF NOT EXISTS test_results (
id TEXT PRIMARY KEY, -- UUID
test_run_id TEXT NOT NULL,
parameter TEXT NOT NULL,
value REAL NOT NULL,
unit TEXT,
lower_limit REAL,
upper_limit REAL,
passed INTEGER NOT NULL, -- 0 or 1
measured_at TEXT NOT NULL, -- ISO8601 timestamp
FOREIGN KEY (test_run_id) REFERENCES test_runs(id)
);
-- Indexes
CREATE INDEX IF NOT EXISTS idx_test_runs_status ON test_runs(status);
CREATE INDEX IF NOT EXISTS idx_test_runs_name ON test_runs(test_name);
CREATE INDEX IF NOT EXISTS idx_test_results_run ON test_results(test_run_id);
CREATE INDEX IF NOT EXISTS idx_test_results_param ON test_results(parameter);
```
### 7.2 Parquet Schema (Time-Series Measurements)
```
File: data/measurements/run_<uuid>/measurements.parquet
Schema:
├── timestamp: float64 # Seconds since epoch (high precision)
├── parameter: string # Measurement parameter name
├── value: float64 # Measured value
├── unit: string # Unit of measurement
├── temperature: float64 # Chamber temperature at measurement
├── input_voltage: float64 # DUT input voltage at measurement
├── load_current: float64 # DUT load current at measurement
```
### 7.3 Data Repository Interface
```python
# py_dvt_ate/data/repository.py (interface)
from typing import Protocol
from uuid import UUID
class ITestRepository(Protocol):
"""Repository interface for test data."""
def create_run(
self,
test_name: str,
config: dict,
operator: str | None = None
) -> UUID:
"""Create a new test run, return its ID."""
...
def update_run_status(self, run_id: UUID, status: str) -> None:
"""Update test run status."""
...
def complete_run(self, run_id: UUID, status: str) -> None:
"""Mark test run as complete with final status."""
...
def save_result(
self,
run_id: UUID,
parameter: str,
value: float,
unit: str,
lower_limit: float | None = None,
upper_limit: float | None = None
) -> None:
"""Save a test result."""
...
def save_measurements(
self,
run_id: UUID,
measurements: list["Measurement"]
) -> None:
"""Save batch of measurements to Parquet."""
...
def get_run(self, run_id: UUID) -> "TestRun":
"""Get test run by ID."""
...
def get_results(self, run_id: UUID) -> list["TestResult"]:
"""Get all results for a test run."""
...
def get_measurements_dataframe(self, run_id: UUID):
"""Get measurements as pandas DataFrame."""
...
```
---
## 8. Configuration Schema
### 8.1 Configuration File Structure
```yaml
# config/default.yaml
# Instrument backend configuration
instruments:
backend: simulator # "simulator" or "pyvisa"
simulator:
host: localhost
thermal_chamber_port: 5001
power_supply_port: 5002
multimeter_port: 5003
pyvisa:
thermal_chamber: "TCPIP::192.168.1.10::5001::SOCKET"
power_supply: "TCPIP::192.168.1.11::5002::SOCKET"
multimeter: "TCPIP::192.168.1.12::5003::SOCKET"
# Physics simulation parameters
physics:
update_rate_hz: 100
thermal:
chamber_time_constant_s: 30.0
case_time_constant_s: 5.0
theta_jc: 15.0 # °C/W
theta_ca: 5.0 # °C/W
chamber:
ramp_rate_c_per_min: 10.0
stability_window_c: 0.5
stability_time_s: 30.0
# DUT model configuration
dut:
model: ldo
parameters:
nominal_output_voltage: 3.3
tempco_ppm_per_c: 50
quiescent_current_ua: 50
quiescent_current_tempco: 0.003
dropout_voltage: 0.3
# Data storage paths
data:
database_path: "./data/py_dvt_ate.db"
measurements_dir: "./data/measurements"
reports_dir: "./data/reports"
# Logging configuration
logging:
level: INFO
file: "./data/logs/py_dvt_ate.log"
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# Dashboard (Streamlit)
dashboard:
enabled: true
port: 8501
# API server (optional, Phase 2)
api:
enabled: false
host: "0.0.0.0"
port: 8000
```
### 8.2 Pydantic Configuration Models
```python
# py_dvt_ate/app/config.py (config models)
from pydantic import BaseModel, Field
from typing import Literal
class SimulatorConfig(BaseModel):
host: str = "localhost"
thermal_chamber_port: int = 5001
power_supply_port: int = 5002
multimeter_port: int = 5003
class PyVISAConfig(BaseModel):
thermal_chamber: str | None = None
power_supply: str | None = None
multimeter: str | None = None
class InstrumentsConfig(BaseModel):
backend: Literal["simulator", "pyvisa"] = "simulator"
simulator: SimulatorConfig = Field(default_factory=SimulatorConfig)
pyvisa: PyVISAConfig = Field(default_factory=PyVISAConfig)
class ThermalConfig(BaseModel):
chamber_time_constant_s: float = 30.0
case_time_constant_s: float = 5.0
theta_jc: float = 15.0
theta_ca: float = 5.0
class ChamberConfig(BaseModel):
ramp_rate_c_per_min: float = 10.0
stability_window_c: float = 0.5
stability_time_s: float = 30.0
class PhysicsConfig(BaseModel):
update_rate_hz: float = 100.0
thermal: ThermalConfig = Field(default_factory=ThermalConfig)
chamber: ChamberConfig = Field(default_factory=ChamberConfig)
class DUTParameters(BaseModel):
nominal_output_voltage: float = 3.3
tempco_ppm_per_c: float = 50.0
quiescent_current_ua: float = 50.0
quiescent_current_tempco: float = 0.003
dropout_voltage: float = 0.3
class DUTConfig(BaseModel):
model: str = "ldo"
parameters: DUTParameters = Field(default_factory=DUTParameters)
class DataConfig(BaseModel):
database_path: str = "./data/py_dvt_ate.db"
measurements_dir: str = "./data/measurements"
reports_dir: str = "./data/reports"
class LoggingConfig(BaseModel):
level: str = "INFO"
file: str = "./data/logs/py_dvt_ate.log"
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
class DashboardConfig(BaseModel):
enabled: bool = True
port: int = 8501
class APIConfig(BaseModel):
enabled: bool = False
host: str = "0.0.0.0"
port: int = 8000
class AppConfig(BaseModel):
"""Root configuration model."""
instruments: InstrumentsConfig = Field(default_factory=InstrumentsConfig)
physics: PhysicsConfig = Field(default_factory=PhysicsConfig)
dut: DUTConfig = Field(default_factory=DUTConfig)
data: DataConfig = Field(default_factory=DataConfig)
logging: LoggingConfig = Field(default_factory=LoggingConfig)
dashboard: DashboardConfig = Field(default_factory=DashboardConfig)
api: APIConfig = Field(default_factory=APIConfig)
```
---
## 9. API Specification
### 9.1 REST Endpoints (Phase 2)
| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/v1/health` | Health check |
| GET | `/api/v1/instruments` | List instrument status |
| GET | `/api/v1/instruments/{id}` | Get instrument details |
| GET | `/api/v1/tests` | List available tests |
| GET | `/api/v1/tests/{name}` | Get test configuration schema |
| POST | `/api/v1/runs` | Start a test run |
| GET | `/api/v1/runs` | List test runs |
| GET | `/api/v1/runs/{id}` | Get test run status |
| GET | `/api/v1/runs/{id}/results` | Get test results |
| GET | `/api/v1/runs/{id}/measurements` | Get measurements (paginated) |
| DELETE | `/api/v1/runs/{id}` | Delete test run |
### 9.2 WebSocket Events (Phase 2)
| Event | Direction | Payload | Description |
|-------|-----------|---------|-------------|
| `instrument.status` | Server→Client | `{id, connected, state}` | Instrument status update |
| `run.progress` | Server→Client | `{run_id, step, total, message}` | Test progress |
| `run.measurement` | Server→Client | `{run_id, parameter, value}` | New measurement |
| `run.complete` | Server→Client | `{run_id, status, summary}` | Test completed |
---
## 10. Development Phases
### 10.1 Phase 1: Vertical Slice (MVP)
**Goal**: End-to-end "Virtual Lab Bench" - Physics Engine → HAL → Driver → Streamlit UI.
**Deliverables**:
- [ ] Project scaffolding (pyproject.toml, directory structure)
- [ ] Physics engine with LDO DUT model
- [ ] Thermal chamber simulator with SCPI
- [ ] Power supply simulator with SCPI
- [ ] DMM simulator with SCPI
- [ ] TCP server for instruments
- [ ] Transport layer (TCP client)
- [ ] SCPI drivers for all instruments
- [ ] HAL interfaces and implementations
- [ ] Instrument factory with simulator backend
- [ ] Basic CLI for manual instrument control
- [ ] Streamlit dashboard with live instrument visualisation
- [ ] SQLite + Parquet data persistence
- [ ] TempCo characterisation test
- [ ] Unit tests for core modules (≥80% coverage)
**Acceptance Criteria**:
- Can start simulation server with single command
- Can observe instruments in Streamlit dashboard
- Can execute TempCo test via CLI
- Results show temperature-dependent behaviour
- Self-heating effect visible in results
- Physics coupling demonstrated end-to-end
---
### 10.2 Phase 2: Test Framework & API
**Goal**: Complete test executive with REST API.
**Deliverables**:
- [ ] Test sequencer with configuration
- [ ] Limit checking engine
- [ ] Load Regulation vs Temperature test
- [ ] FastAPI REST endpoints
- [ ] WebSocket real-time updates
- [ ] OpenAPI documentation
- [ ] Test configuration via YAML
- [ ] CSV export
**Acceptance Criteria**:
- Can run multi-test sequences
- API fully documented
- Can start tests via API
- Real-time updates in Streamlit
---
### 10.3 Phase 3: Reporting & Polish
**Goal**: Professional reporting and production readiness.
**Deliverables**:
- [ ] PDF report generation
- [ ] HTML report generation
- [ ] Docker Compose deployment
- [ ] GitHub Actions CI/CD
- [ ] Comprehensive README
- [ ] Architecture documentation
- [ ] Demo scripts with sample output
- [ ] Screenshots/GIFs for README
**Acceptance Criteria**:
- Professional reports with charts
- One-command startup (docker-compose up)
- All documentation complete
- Impressive demo scenario
---
## Appendix A: Technology Versions
| Technology | Version | Purpose |
|------------|---------|---------|
| Python | 3.11+ | Runtime |
| NumPy | ≥1.24 | Numerical |
| SciPy | ≥1.11 | Scientific |
| Pydantic | ≥2.0 | Config |
| Typer | ≥0.9 | CLI |
| Streamlit | ≥1.28 | Dashboard |
| FastAPI | ≥0.100 | API (Phase 2) |
| PyArrow | ≥14.0 | Parquet |
| pytest | ≥7.0 | Testing |
| Ruff | ≥0.1 | Linting |
| mypy | ≥1.0 | Types |
---
## Appendix B: Dependencies (pyproject.toml)
```toml
[project]
name = "py_dvt_ate"
version = "0.1.0"
description = "Coupled Physics DVT Simulation Platform"
requires-python = ">=3.11"
dependencies = [
"numpy>=1.24",
"scipy>=1.11",
"pydantic>=2.0",
"pyyaml>=6.0",
"typer>=0.9",
"rich>=13.0",
"pyarrow>=14.0",
"streamlit>=1.28",
"pandas>=2.0",
"plotly>=5.18",
]
[project.optional-dependencies]
api = [
"fastapi>=0.100",
"uvicorn>=0.23",
"websockets>=11.0",
]
reports = [
"jinja2>=3.1",
"weasyprint>=60.0",
]
dev = [
"pytest>=7.0",
"pytest-cov>=4.0",
"pytest-asyncio>=0.21",
"ruff>=0.1",
"mypy>=1.0",
]
[project.scripts]
py_dvt_ate = "py_dvt_ate.cli.main:app"
py_dvt_ate-server = "py_dvt_ate.server.main:main"
py_dvt_ate-dashboard = "py_dvt_ate.dashboard.app:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.mypy]
python_version = "3.11"
strict = true
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
```
---
**End of Technical Design Document**