- Reorder sprints for visual-first development - Dashboard (Sprint 4) now follows Physics Engine (Sprint 3) - Infrastructure layers (SCPI, TCP, HAL) follow visual demo - Update project references to py-dvt-ate
1675 lines
65 KiB
Markdown
1675 lines
65 KiB
Markdown
# 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
|
||
|
||
```
|
||
thermaulate/
|
||
├── 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
|
||
│
|
||
├── src/
|
||
│ └── thermaulate/
|
||
│ ├── __init__.py
|
||
│ ├── py.typed # PEP 561 marker
|
||
│ │
|
||
│ ├── physics/ # Physics simulation engine
|
||
│ │ ├── __init__.py
|
||
│ │ ├── engine.py # Main physics loop
|
||
│ │ ├── thermal.py # Thermal domain model
|
||
│ │ ├── electrical.py # Electrical domain model
|
||
│ │ └── dut/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── base.py # DUT base class
|
||
│ │ └── ldo.py # LDO voltage regulator model
|
||
│ │
|
||
│ ├── instruments/ # Virtual instrument implementations
|
||
│ │ ├── __init__.py
|
||
│ │ ├── base.py # Instrument base class
|
||
│ │ ├── scpi_parser.py # SCPI command parser
|
||
│ │ ├── thermal_chamber.py # Thermal chamber simulator
|
||
│ │ ├── power_supply.py # Power supply simulator
|
||
│ │ └── multimeter.py # DMM simulator
|
||
│ │
|
||
│ ├── server/ # Simulation server
|
||
│ │ ├── __init__.py
|
||
│ │ ├── tcp_server.py # Async TCP server
|
||
│ │ └── main.py # Server entry point
|
||
│ │
|
||
│ ├── transport/ # Communication layer
|
||
│ │ ├── __init__.py
|
||
│ │ ├── base.py # Transport protocol
|
||
│ │ ├── tcp.py # TCP/IP implementation
|
||
│ │ └── async_tcp.py # Async TCP implementation
|
||
│ │
|
||
│ ├── drivers/ # Instrument SCPI drivers
|
||
│ │ ├── __init__.py
|
||
│ │ ├── base.py # Driver base class
|
||
│ │ ├── thermal_chamber.py # Chamber SCPI driver
|
||
│ │ ├── power_supply.py # PSU SCPI driver
|
||
│ │ └── multimeter.py # DMM SCPI driver
|
||
│ │
|
||
│ ├── hal/ # Hardware Abstraction Layer
|
||
│ │ ├── __init__.py
|
||
│ │ ├── interfaces.py # Protocol definitions
|
||
│ │ ├── factory.py # Instrument factory
|
||
│ │ └── impl/ # HAL implementations
|
||
│ │ ├── __init__.py
|
||
│ │ ├── thermal_chamber.py
|
||
│ │ ├── power_supply.py
|
||
│ │ └── multimeter.py
|
||
│ │
|
||
│ ├── executive/ # Test execution framework
|
||
│ │ ├── __init__.py
|
||
│ │ ├── sequencer.py # Test sequencer
|
||
│ │ ├── context.py # Test context
|
||
│ │ ├── logger.py # Test logger
|
||
│ │ ├── limits.py # Limit checker
|
||
│ │ └── models.py # Domain models
|
||
│ │
|
||
│ ├── tests/ # DVT test implementations
|
||
│ │ ├── __init__.py
|
||
│ │ ├── base.py # Test base class
|
||
│ │ ├── tempco.py # TempCo characterisation
|
||
│ │ └── load_regulation.py # Load regulation test
|
||
│ │
|
||
│ ├── data/ # Data persistence
|
||
│ │ ├── __init__.py
|
||
│ │ ├── repository.py # Data access layer
|
||
│ │ ├── models.py # Data models
|
||
│ │ └── migrations/ # Schema migrations
|
||
│ │
|
||
│ ├── reporting/ # Report generation (Phase 3)
|
||
│ │ ├── __init__.py
|
||
│ │ ├── generator.py # Report generator
|
||
│ │ ├── pdf.py # PDF output
|
||
│ │ ├── html.py # HTML output
|
||
│ │ └── templates/ # Report templates
|
||
│ │
|
||
│ ├── api/ # REST API (Phase 2)
|
||
│ │ ├── __init__.py
|
||
│ │ ├── main.py # FastAPI app
|
||
│ │ └── routes/
|
||
│ │ ├── __init__.py
|
||
│ │ ├── instruments.py
|
||
│ │ ├── tests.py
|
||
│ │ └── runs.py
|
||
│ │
|
||
│ ├── dashboard/ # Streamlit dashboard
|
||
│ │ ├── __init__.py
|
||
│ │ ├── app.py # Main Streamlit app
|
||
│ │ ├── pages/ # Multi-page app
|
||
│ │ │ ├── 01_instruments.py
|
||
│ │ │ ├── 02_run_test.py
|
||
│ │ │ └── 03_results.py
|
||
│ │ └── components/ # Reusable UI components
|
||
│ │ ├── __init__.py
|
||
│ │ └── instrument_panel.py
|
||
│ │
|
||
│ ├── cli/ # Command-line interface
|
||
│ │ ├── __init__.py
|
||
│ │ └── main.py # Typer CLI app
|
||
│ │
|
||
│ └── config/ # Configuration handling
|
||
│ ├── __init__.py
|
||
│ ├── models.py # Pydantic config models
|
||
│ └── loader.py # Config file loader
|
||
│
|
||
├── tests/ # Test suite
|
||
│ ├── conftest.py # pytest fixtures
|
||
│ ├── unit/
|
||
│ │ ├── test_physics_engine.py
|
||
│ │ ├── test_scpi_parser.py
|
||
│ │ ├── test_thermal_model.py
|
||
│ │ └── ...
|
||
│ └── integration/
|
||
│ ├── test_instrument_communication.py
|
||
│ ├── test_tempco_sequence.py
|
||
│ └── ...
|
||
│
|
||
├── config/ # Configuration files
|
||
│ ├── default.yaml # Default configuration
|
||
│ └── example_pyvisa.yaml # Example for real hardware
|
||
│
|
||
├── docker/
|
||
│ ├── Dockerfile.server # Simulation server image
|
||
│ ├── Dockerfile.app # Test application image
|
||
│ └── docker-compose.yml # Full stack orchestration
|
||
│
|
||
└── scripts/
|
||
├── demo.py # Demo script
|
||
└── run_tempco.py # Example test execution
|
||
```
|
||
|
||
### 2.2 Package Dependencies
|
||
|
||
```
|
||
thermaulate/
|
||
├── cli/ ──────────────────────────────────────────────┐
|
||
├── api/ ──────────────────────────────────────────────┤
|
||
├── dashboard/ ──────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ PRESENTATION │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
├── executive/ ◄───────────────────────────────────────────────┤
|
||
├── tests/ ◄───────────────────────────────────────────────┤
|
||
├── reporting/ ◄───────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ APPLICATION │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ ▼ │
|
||
├── hal/interfaces ◄───────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ DOMAIN (Abstractions) │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │ │
|
||
│ implements│ │
|
||
│ ▼ │
|
||
├── hal/impl ◄───────────────────────────────────────────────┤
|
||
├── drivers/ ◄───────────────────────────────────────────────┤
|
||
├── transport/ ◄───────────────────────────────────────────────┤
|
||
├── data/ ◄───────────────────────────────────────────────┤
|
||
│ │
|
||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||
│ │ INFRASTRUCTURE │ │
|
||
│ └─────────────────────────────────────────────────────────┘ │
|
||
│ │
|
||
└─────────────────────────────────────────────────────────────────┘
|
||
|
||
SIMULATION SERVER (Separate Process):
|
||
├── physics/ ◄─── Pure domain logic, no external dependencies
|
||
├── instruments/ ◄─── Depends on physics
|
||
└── server/ ◄─── Depends on instruments
|
||
```
|
||
|
||
---
|
||
|
||
## 3. Module Specifications
|
||
|
||
### 3.1 Physics Module
|
||
|
||
**Responsibility**: Simulate coupled thermal-electrical behaviour.
|
||
|
||
**Key Components**:
|
||
|
||
| Component | File | Purpose |
|
||
|-----------|------|---------|
|
||
| PhysicsEngine | `engine.py` | Main simulation loop, state management |
|
||
| ThermalModel | `thermal.py` | Heat transfer calculations |
|
||
| ElectricalModel | `electrical.py` | Current/voltage relationships |
|
||
| DUTBase | `dut/base.py` | Abstract DUT interface |
|
||
| LDOModel | `dut/ldo.py` | LDO voltage regulator implementation |
|
||
|
||
**State Management**:
|
||
- Engine maintains global simulation time
|
||
- State updates at fixed timestep (default 10ms = 100Hz)
|
||
- Immutable state snapshots returned to callers
|
||
|
||
---
|
||
|
||
### 3.2 Instruments Module
|
||
|
||
**Responsibility**: SCPI-compliant virtual instrument behaviour.
|
||
|
||
**Key Components**:
|
||
|
||
| Component | File | Purpose |
|
||
|-----------|------|---------|
|
||
| InstrumentBase | `base.py` | Common instrument functionality |
|
||
| SCPIParser | `scpi_parser.py` | Parse SCPI command strings |
|
||
| ThermalChamberSim | `thermal_chamber.py` | Chamber simulation |
|
||
| PowerSupplySim | `power_supply.py` | PSU simulation |
|
||
| MultimeterSim | `multimeter.py` | DMM simulation |
|
||
|
||
**Command Processing Flow**:
|
||
```
|
||
SCPI String → Parser → Command Object → Instrument Handler → Response
|
||
```
|
||
|
||
---
|
||
|
||
### 3.3 Transport Module
|
||
|
||
**Responsibility**: Low-level communication.
|
||
|
||
**Key Components**:
|
||
|
||
| Component | File | Purpose |
|
||
|-----------|------|---------|
|
||
| Transport Protocol | `base.py` | Abstract transport interface |
|
||
| TCPTransport | `tcp.py` | Synchronous TCP implementation |
|
||
| AsyncTCPTransport | `async_tcp.py` | Async TCP implementation |
|
||
|
||
---
|
||
|
||
### 3.4 Drivers Module
|
||
|
||
**Responsibility**: Instrument-specific SCPI command sets.
|
||
|
||
**Key Components**:
|
||
|
||
| Component | File | Purpose |
|
||
|-----------|------|---------|
|
||
| DriverBase | `base.py` | Common driver functionality |
|
||
| ThermalChamberDriver | `thermal_chamber.py` | Chamber SCPI commands |
|
||
| PowerSupplyDriver | `power_supply.py` | PSU SCPI commands |
|
||
| MultimeterDriver | `multimeter.py` | DMM SCPI commands |
|
||
|
||
---
|
||
|
||
### 3.5 HAL Module
|
||
|
||
**Responsibility**: Hardware abstraction interfaces.
|
||
|
||
**Key Components**:
|
||
|
||
| Component | File | Purpose |
|
||
|-----------|------|---------|
|
||
| Protocols | `interfaces.py` | Abstract interfaces |
|
||
| InstrumentFactory | `factory.py` | Creates instrument sets from config |
|
||
| HAL Implementations | `impl/*.py` | Concrete HAL classes |
|
||
|
||
---
|
||
|
||
### 3.6 Executive Module
|
||
|
||
**Responsibility**: Test execution orchestration.
|
||
|
||
**Key Components**:
|
||
|
||
| Component | File | Purpose |
|
||
|-----------|------|---------|
|
||
| TestSequencer | `sequencer.py` | Run test sequences |
|
||
| TestContext | `context.py` | Runtime context |
|
||
| TestLogger | `logger.py` | Measurement logging |
|
||
| LimitChecker | `limits.py` | Pass/fail evaluation |
|
||
| Domain Models | `models.py` | Measurement, Result, etc. |
|
||
|
||
---
|
||
|
||
### 3.7 Dashboard Module
|
||
|
||
**Responsibility**: Real-time visualisation via Streamlit.
|
||
|
||
**Key Components**:
|
||
|
||
| Component | File | Purpose |
|
||
|-----------|------|---------|
|
||
| Main App | `app.py` | Streamlit application entry point |
|
||
| Instruments Page | `pages/01_instruments.py` | Live instrument status |
|
||
| Run Test Page | `pages/02_run_test.py` | Test execution interface |
|
||
| Results Page | `pages/03_results.py` | Historical results viewer |
|
||
| Instrument Panel | `components/instrument_panel.py` | Reusable instrument display |
|
||
|
||
---
|
||
|
||
## 4. Interface Definitions
|
||
|
||
### 4.1 HAL Interfaces
|
||
|
||
```python
|
||
# thermaulate/hal/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
|
||
# thermaulate/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
|
||
# thermaulate/executive/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
|
||
# thermaulate/hal/factory.py
|
||
|
||
from dataclasses import dataclass
|
||
from typing import Literal
|
||
|
||
from thermaulate.hal.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 thermaulate.transport.tcp import TCPTransport
|
||
from thermaulate.drivers.thermal_chamber import ThermalChamberDriver
|
||
from thermaulate.drivers.power_supply import PowerSupplyDriver
|
||
from thermaulate.drivers.multimeter import MultimeterDriver
|
||
from thermaulate.hal.impl.thermal_chamber import ThermalChamberHAL
|
||
from thermaulate.hal.impl.power_supply import PowerSupplyHAL
|
||
from thermaulate.hal.impl.multimeter import MultimeterHAL
|
||
|
||
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=ThermalChamberHAL(ThermalChamberDriver(chamber_transport)),
|
||
psu=PowerSupplyHAL(PowerSupplyDriver(psu_transport)),
|
||
dmm=MultimeterHAL(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
|
||
# thermaulate/instruments/scpi_parser.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
|
||
# thermaulate/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
|
||
# thermaulate/data/repository.py
|
||
|
||
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/thermaulate.db"
|
||
measurements_dir: "./data/measurements"
|
||
reports_dir: "./data/reports"
|
||
|
||
# Logging configuration
|
||
logging:
|
||
level: INFO
|
||
file: "./data/logs/thermaulate.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
|
||
# thermaulate/config/models.py
|
||
|
||
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/thermaulate.db"
|
||
measurements_dir: str = "./data/measurements"
|
||
reports_dir: str = "./data/reports"
|
||
|
||
|
||
class LoggingConfig(BaseModel):
|
||
level: str = "INFO"
|
||
file: str = "./data/logs/thermaulate.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 = "thermaulate"
|
||
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]
|
||
thermaulate = "thermaulate.cli.main:app"
|
||
thermaulate-server = "thermaulate.server.main:main"
|
||
thermaulate-dashboard = "thermaulate.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**
|