commit 2ca5144eaf9698b0990f4ef80eba765958a6ac1f Author: Kai Chappell Date: Wed Jan 15 10:17:15 2025 +0000 Initial project setup with documentation - Add project requirements document (01_requirements.md) - Add technical specification (02_technical_specification.md) - Add architecture decisions (03_architecture_decisions.md) - Add README with project overview - Add .gitignore for Python projects diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b62fa8c --- /dev/null +++ b/.gitignore @@ -0,0 +1,58 @@ + +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Virtual environments +.venv/ +venv/ +ENV/ +env/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Testing +.pytest_cache/ +.coverage +htmlcov/ +.tox/ +.nox/ + +# Type checking +.mypy_cache/ + +# Data (local development) +data/ +*.db +*.parquet + +# Logs +*.log +logs/ + +# OS +.DS_Store +Thumbs.db diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e2ade3 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# py_dvt_ate + +**ThermalATE: Coupled Physics DVT Simulation Platform** + +A software simulation environment that accurately models the physical coupling between thermal and electrical domains, enabling DVT (Design Validation Test) engineers to develop, validate, and debug characterisation test sequences without physical access to laboratory equipment. + +## Overview + +ThermalATE enables offline development of ATE (Automated Test Equipment) characterisation algorithms by simulating: + +- **Thermal Chamber** - Temperature control with realistic ramp and settling behaviour +- **Programmable Power Supply** - Voltage/current control and measurement +- **Digital Multimeter** - DC voltage measurement with configurable integration time +- **DUT Models** - Device Under Test simulation with thermal-electrical coupling (e.g., LDO voltage regulators) + +## Key Features + +- **Coupled Physics Simulation** - DUT self-heating affects electrical parameters realistically +- **SCPI Protocol** - Industry-standard commands for instrument control +- **Hardware Abstraction** - Same test code works with simulated or real instruments +- **Multiple Interfaces** - CLI, programmatic API, and Streamlit dashboard +- **Data Persistence** - SQLite for metadata, Parquet for time-series measurements + +## Documentation + +| Document | Purpose | +|----------|---------| +| [Requirements](docs/01_requirements.md) | Defines **what** the system must do | +| [Technical Specification](docs/02_technical_specification.md) | Specifies **how** to implement the system | +| [Architecture Decisions](docs/03_architecture_decisions.md) | Explains **why** decisions were made | + +## Project Status + +**Status:** In Development + +This project is currently being developed. See the requirements document for the full scope and success criteria. + +## Technology Stack + +- **Language:** Python 3.11+ +- **Physics:** NumPy, SciPy +- **Configuration:** Pydantic, YAML +- **CLI:** Typer +- **Dashboard:** Streamlit +- **Data:** SQLite, PyArrow (Parquet) + +## Author + +Kai Chappell + +## Licence + +TBD diff --git a/docs/01_requirements.md b/docs/01_requirements.md new file mode 100644 index 0000000..d2526ac --- /dev/null +++ b/docs/01_requirements.md @@ -0,0 +1,362 @@ +# Business Requirements Document +## ThermalATE: Coupled Physics DVT Simulation Platform + +| Document ID | BRD-001 | +|-------------|---------| +| Version | 1.1.0 | +| Status | Draft | +| Author | Kai Chappell | +| Created | 2025-12-01 | +| Last Updated | 2025-12-01 | + +--- + +## Purpose + +This document defines **what** the ThermalATE system must do. It specifies the functional and non-functional requirements, constraints, and success criteria. + +For **why** decisions were made, see `03_architecture_decisions.md`. +For **how** to implement the system, see `02_technical_specification.md`. + +--- + +## Related Documents + +| Document | Purpose | +|----------|---------| +| `01_requirements.md` | Defines **what** the system must do (this document) | +| `02_technical_specification.md` | Specifies **how** to implement the system | +| `03_architecture_decisions.md` | Explains **why** decisions were made | + +--- + +## Table of Contents + +1. [Executive Summary](#1-executive-summary) +2. [Problem Statement](#2-problem-statement) +3. [Business Objectives](#3-business-objectives) +4. [Stakeholders](#4-stakeholders) +5. [Scope](#5-scope) +6. [Functional Requirements](#6-functional-requirements) +7. [Non-Functional Requirements](#7-non-functional-requirements) +8. [Constraints](#8-constraints) +9. [Assumptions](#9-assumptions) +10. [Success Criteria](#10-success-criteria) +11. [Risks](#11-risks) +12. [Glossary](#12-glossary) + +--- + +## 1. Executive Summary + +### 1.1 Purpose + +ThermalATE is a coupled-physics Device Validation Test (DVT) simulation platform that enables engineers to develop, validate, and debug characterisation test sequences without physical access to laboratory equipment. + +### 1.2 Background + +DVT engineers require access to expensive Automated Test Equipment (ATE) to develop and validate characterisation algorithms. Physical equipment is a shared resource with limited availability, creating bottlenecks in the development process. + +### 1.3 Proposed Solution + +A software simulation environment that accurately models the physical coupling between thermal and electrical domains, allowing complete test sequence development and validation without hardware access. + +--- + +## 2. Problem Statement + +### 2.1 Current Challenges + +| Challenge | Impact | +|-----------|--------| +| **Limited Equipment Access** | ATE racks cost $100k+ and are shared resources; engineers must schedule lab time | +| **Slow Development Iteration** | Cannot iterate on test algorithms without physical equipment reservation | +| **Difficult Edge Case Testing** | Testing failure modes or boundary conditions risks damaging equipment | +| **Training Barriers** | New engineers cannot practise without supervision on expensive equipment | +| **Remote Work Limitations** | Engineers cannot develop tests without physical lab presence | + +### 2.2 Gap Analysis + +| Need | Current State | Desired State | +|------|---------------|---------------| +| Algorithm Development | Requires physical equipment | Fully offline development | +| Thermal-Electrical Coupling | Not modelled in simple mocks | Realistic coupled physics | +| Hardware Transition | Separate code for simulation vs real | Single codebase, configuration-driven | +| Test Validation | Only possible in lab | Complete validation before lab time | + +--- + +## 3. Business Objectives + +### 3.1 Primary Objectives + +| ID | Objective | Measurable Outcome | +|----|-----------|-------------------| +| BO-1 | Enable offline DVT algorithm development | Engineers can develop complete characterisation tests without equipment access | +| BO-2 | Reduce equipment contention | Test code validated before consuming lab time | +| BO-3 | Demonstrate professional software engineering | Portfolio-quality project following industry best practices | + +### 3.2 Secondary Objectives + +| ID | Objective | Measurable Outcome | +|----|-----------|-------------------| +| BO-4 | Provide training platform | Safe environment for learning ATE concepts | +| BO-5 | Enable remote development | Full DVT development capability without lab presence | + +--- + +## 4. Stakeholders + +| Stakeholder | Role | Interest | +|-------------|------|----------| +| DVT Engineer (Primary User) | Develops characterisation tests | Needs realistic simulation for algorithm validation | +| Test Manager | Oversees test development | Wants reduced equipment contention, faster delivery | +| New Engineers | Learning ATE workflows | Needs safe practice environment | +| Portfolio Reviewer | Evaluates project quality | Assesses software engineering competence | + +--- + +## 5. Scope + +### 5.1 In Scope + +| Category | Items | +|----------|-------| +| **Simulated Instruments** | Thermal Chamber, Programmable Power Supply, Digital Multimeter | +| **DUT Models** | Voltage Regulator (LDO) with thermal-electrical coupling | +| **Test Types** | Temperature Coefficient characterisation, Load Regulation | +| **User Interfaces** | Command-line interface, Streamlit dashboard, Programmatic API | +| **Data Management** | Test result persistence, Data export | +| **Reporting** | Automated test reports (future phase) | + +### 5.2 Out of Scope + +| Category | Items | Rationale | +|----------|-------|-----------| +| Additional Instruments | Oscilloscope, Signal Generator | Future phase | +| Additional DUT Models | Op-Amp, ADC, MOSFET | Future phase | +| Multi-user Operation | Concurrent users, access control | Not required for portfolio | +| Cloud Deployment | Hosted service | Local execution sufficient | +| Legacy Protocols | GPIB, USB-TMC hardware | Focus on TCP/IP simulation | + +### 5.3 Boundaries + +The system SHALL: +- Operate as a standalone local application +- Simulate instrument behaviour, not replicate specific vendor implementations +- Model physics at a level sufficient for algorithm validation, not device design + +--- + +## 6. Functional Requirements + +### 6.1 Physics Simulation + +| ID | Requirement | Priority | +|----|-------------|----------| +| FR-PS-1 | The system SHALL simulate thermal chamber temperature control with realistic ramp and settling behaviour | Must | +| FR-PS-2 | The system SHALL model thermal coupling between chamber ambient and DUT case temperature | Must | +| FR-PS-3 | The system SHALL model DUT self-heating based on power dissipation | Must | +| FR-PS-4 | The system SHALL calculate DUT junction temperature from case temperature and thermal resistance | Must | +| FR-PS-5 | The system SHALL compute DUT electrical parameters as functions of junction temperature | Must | +| FR-PS-6 | The system SHALL update physics state at a rate sufficient for realistic transient behaviour | Must | +| FR-PS-7 | The system SHALL model thermal time constants for chamber and DUT thermal mass | Should | + +### 6.2 Instrument Simulation + +| ID | Requirement | Priority | +|----|-------------|----------| +| FR-IS-1 | The system SHALL provide a simulated Thermal Chamber with temperature setpoint and readback | Must | +| FR-IS-2 | The system SHALL provide a simulated Power Supply with voltage/current control and measurement | Must | +| FR-IS-3 | The system SHALL provide a simulated Multimeter with DC voltage measurement | Must | +| FR-IS-4 | All simulated instruments SHALL respond to industry-standard SCPI commands | Must | +| FR-IS-5 | Instruments SHALL communicate over TCP/IP network sockets | Must | +| FR-IS-6 | Instruments SHALL maintain state between commands within a session | Must | +| FR-IS-7 | Instruments SHALL return measurement values derived from the physics simulation | Must | +| FR-IS-8 | Instruments SHALL simulate realistic timing (settling, acquisition delays) | Should | + +### 6.3 Hardware Abstraction + +| ID | Requirement | Priority | +|----|-------------|----------| +| FR-HA-1 | The system SHALL provide abstract interfaces for each instrument type | Must | +| FR-HA-2 | Test code SHALL interact only with abstract interfaces, not concrete implementations | Must | +| FR-HA-3 | The system SHALL support swapping between simulated and real instruments via configuration | Must | +| FR-HA-4 | The abstraction layer SHALL not expose simulation-specific or hardware-specific details | Must | + +### 6.4 Test Execution + +| ID | Requirement | Priority | +|----|-------------|----------| +| FR-TE-1 | The system SHALL support defining multi-step test sequences | Must | +| FR-TE-2 | The system SHALL execute test sequences with proper instrument coordination | Must | +| FR-TE-3 | The system SHALL log all measurements during test execution | Must | +| FR-TE-4 | The system SHALL evaluate measurements against defined limits | Must | +| FR-TE-5 | The system SHALL determine overall pass/fail status for tests | Must | +| FR-TE-6 | The system SHALL support Temperature Coefficient characterisation test | Must | +| FR-TE-7 | The system SHALL support Load Regulation vs Temperature test | Should | + +### 6.5 Data Management + +| ID | Requirement | Priority | +|----|-------------|----------| +| FR-DM-1 | The system SHALL persist test run metadata (test name, timestamp, status) | Must | +| FR-DM-2 | The system SHALL persist all measurements with timestamps and conditions | Must | +| FR-DM-3 | The system SHALL persist calculated results with pass/fail status | Must | +| FR-DM-4 | The system SHALL support querying historical test results | Should | +| FR-DM-5 | The system SHALL support exporting data in common formats (CSV) | Should | + +### 6.6 Reporting + +| ID | Requirement | Priority | +|----|-------------|----------| +| FR-RP-1 | The system SHALL generate test reports containing results and pass/fail status | Could | +| FR-RP-2 | Reports SHALL include data visualisation (charts/graphs) | Could | +| FR-RP-3 | Reports SHALL be exportable in portable format (PDF or HTML) | Could | + +### 6.7 User Interface + +| ID | Requirement | Priority | +|----|-------------|----------| +| FR-UI-1 | The system SHALL provide a command-line interface for test execution | Must | +| FR-UI-2 | The system SHALL provide a programmatic API for integration | Must | +| FR-UI-3 | The system SHALL provide a Streamlit dashboard for real-time monitoring | Should | +| FR-UI-4 | Real-time instrument status SHALL be observable during test execution | Should | + +--- + +## 7. Non-Functional Requirements + +### 7.1 Performance + +| ID | Requirement | Metric | +|----|-------------|--------| +| NFR-P-1 | Physics simulation SHALL run in real-time or faster | Update rate ≥ 100 Hz | +| NFR-P-2 | Instrument command latency SHALL be negligible | < 50 ms round-trip (localhost) | +| NFR-P-3 | Data logging SHALL not impact test execution | No dropped measurements | + +### 7.2 Reliability + +| ID | Requirement | Metric | +|----|-------------|--------| +| NFR-R-1 | System SHALL handle instrument communication errors gracefully | Clear error messages, no crashes | +| NFR-R-2 | Data SHALL be persisted before system exit | No data loss on normal shutdown | +| NFR-R-3 | System SHALL recover from transient connection failures | Auto-reconnect capability | + +### 7.3 Maintainability + +| ID | Requirement | Metric | +|----|-------------|--------| +| NFR-M-1 | Code SHALL follow consistent style and formatting | Automated linting passes | +| NFR-M-2 | Public interfaces SHALL be documented | All public APIs have docstrings | +| NFR-M-3 | Code SHALL have automated test coverage | ≥ 80% coverage on core modules | +| NFR-M-4 | Architecture SHALL support adding new instruments without modifying existing code | Open/closed principle demonstrated | +| NFR-M-5 | Architecture SHALL support adding new DUT models without modifying physics engine | Open/closed principle demonstrated | + +### 7.4 Portability + +| ID | Requirement | Metric | +|----|-------------|--------| +| NFR-PT-1 | System SHALL run on Windows 10+ | Tested and documented | +| NFR-PT-2 | System SHALL run on Linux | Tested and documented | +| NFR-PT-3 | System SHALL require only standard package manager installation | pip install sufficient | + +### 7.5 Usability + +| ID | Requirement | Metric | +|----|-------------|--------| +| NFR-U-1 | System SHALL start with a single command | Documented quick-start | +| NFR-U-2 | Error messages SHALL clearly indicate the problem and suggested resolution | Actionable error messages | +| NFR-U-3 | Configuration SHALL use human-readable format | YAML or similar | + +--- + +## 8. Constraints + +### 8.1 Technical Constraints + +| ID | Constraint | Rationale | +|----|------------|-----------| +| C-T-1 | Implementation SHALL be 100% Python | Target role alignment; industry standard for test automation | +| C-T-2 | System SHALL not require external service dependencies (database servers, message brokers) | Simplify deployment; portfolio reviewability | +| C-T-3 | All dependencies SHALL be open-source | Avoid licensing issues | +| C-T-4 | System SHALL run without requiring physical instruments | Core purpose of the project | + +### 8.2 Project Constraints + +| ID | Constraint | Rationale | +|----|------------|-----------| +| C-P-1 | Single developer | Portfolio project | +| C-P-2 | No budget for commercial tools or services | Personal project | + +--- + +## 9. Assumptions + +| ID | Assumption | Impact if Invalid | +|----|------------|-------------------| +| A-1 | Users have Python 3.11+ installed | Must document installation requirements | +| A-2 | Users have basic familiarity with ATE concepts | May need to add tutorial documentation | +| A-3 | Localhost network performance is sufficient | May need to optimise for slower systems | +| A-4 | Single-user operation is acceptable for MVP | Multi-user would require significant redesign | +| A-5 | Simplified physics models are acceptable for algorithm validation | May need to increase model fidelity | + +--- + +## 10. Success Criteria + +### 10.1 Minimum Viable Product (MVP) + +The project is successful if: + +| ID | Criterion | Verification Method | +|----|-----------|---------------------| +| SC-1 | Can execute a complete Temperature Coefficient characterisation test against simulated instruments | End-to-end test demonstration | +| SC-2 | Physics coupling is evident: DUT self-heating affects measurements | Comparison of low-power vs high-power test results | +| SC-3 | Same test code runs against simulated and real instruments via configuration change | Demonstrate with PyVISA backend (if hardware available) or mock | +| SC-4 | Test results are persisted and retrievable | Query historical results | +| SC-5 | Code follows SOLID principles with clear module boundaries | Architecture review | +| SC-6 | Core modules have ≥ 80% test coverage | Coverage report | + +### 10.2 Full Project Success + +| ID | Criterion | Verification Method | +|----|-----------|---------------------| +| SC-7 | Streamlit dashboard provides real-time instrument visualisation | Dashboard demonstration | +| SC-8 | System deployable with single command | Docker Compose demonstration | +| SC-9 | Complete documentation (README, architecture, API) | Documentation review | +| SC-10 | Demonstrates portfolio-quality engineering | External code review | + +--- + +## 11. Risks + +| ID | Risk | Probability | Impact | Mitigation | +|----|------|-------------|--------|------------| +| R-1 | Physics model too simplistic for realistic behaviour | Medium | High | Research real component datasheets; validate against published specifications | +| R-2 | Scope creep delays completion | High | Medium | Strict MVP definition; defer features to future phases | +| R-3 | SCPI implementation incompatible with real instruments | Low | Medium | Reference IVI Foundation standards | +| R-4 | Over-engineering delays delivery | Medium | Medium | Focus on working software over architectural perfection | +| R-5 | UI development consumes excessive time | Medium | High | CLI-first approach; Streamlit for rapid iteration | + +--- + +## 12. Glossary + +| Term | Definition | +|------|------------| +| **ATE** | Automated Test Equipment - systems for testing electronic devices | +| **Characterisation** | Process of measuring device parameters across operating conditions | +| **DUT** | Device Under Test - the component being characterised | +| **DVT** | Design Validation Test - testing to verify design meets specifications | +| **HAL** | Hardware Abstraction Layer - interface isolating code from hardware specifics | +| **LDO** | Low Dropout Regulator - type of linear voltage regulator | +| **NPLC** | Number of Power Line Cycles - integration time unit for DMMs | +| **SCPI** | Standard Commands for Programmable Instruments - IEEE 488.2 command syntax | +| **TempCo** | Temperature Coefficient - rate of parameter change with temperature (ppm/°C) | +| **Thermal Coupling** | Physical interaction between thermal and electrical domains | +| **θ (theta)** | Thermal resistance, measured in °C/W | + +--- + +**End of Business Requirements Document** diff --git a/docs/02_technical_specification.md b/docs/02_technical_specification.md new file mode 100644 index 0000000..be84341 --- /dev/null +++ b/docs/02_technical_specification.md @@ -0,0 +1,1674 @@ +# Technical Design Document +## ThermalATE: 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 ThermalATE 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?` | `,,,` | Identity query | +| `*RST` | - | Reset to default state | +| `*CLS` | - | Clear status registers | +| `*OPC?` | `1` | Operation complete query | +| `*OPC` | - | Set OPC bit when complete | +| `SYST:ERR?` | `,""` | Query error queue | + +### 5.2 Thermal Chamber Commands + +| Command | Parameters | Response | Description | +|---------|------------|----------|-------------| +| `TEMP:SETPOINT` | `` | - | Set target temperature (°C) | +| `TEMP:SETPOINT?` | - | `` | Query temperature setpoint | +| `TEMP:ACTUAL?` | - | `` | Query actual temperature | +| `TEMP:RAMP:RATE` | `` | - | Set ramp rate (°C/min) | +| `TEMP:RAMP:RATE?` | - | `` | Query ramp rate | +| `TEMP:STAB:WIN` | `` | - | Set stability window (±°C) | +| `TEMP:STAB:TIME` | `` | - | Set stability time (seconds) | +| `TEMP:STAB?` | - | `0` or `1` | Query stability status | + +**Example Session**: +``` +> *IDN? +< ThermalATE,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` | `` | - | Set voltage (selected channel) | +| `VOLT?` | - | `` | Query voltage setpoint | +| `CURR` | `` | - | Set current limit | +| `CURR?` | - | `` | Query current limit | +| `MEAS:VOLT?` | - | `` | Measure actual voltage | +| `MEAS:CURR?` | - | `` | Measure actual current | +| `MEAS:POW?` | - | `` | 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? +< ThermalATE,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` | `[]` | - | Configure DC voltage measurement | +| `CONF:CURR:DC` | `[]` | - | Configure DC current measurement | +| `CONF:RES` | `[]` | - | Configure resistance measurement | +| `MEAS:VOLT:DC?` | `[]` | `` | Measure DC voltage | +| `MEAS:CURR:DC?` | `[]` | `` | Measure DC current | +| `MEAS:RES?` | `[]` | `` | Measure resistance | +| `READ?` | - | `` | Read with current config | +| `SENS:VOLT:DC:NPLC` | `` | - | Set integration time (PLCs) | +| `SENS:VOLT:DC:NPLC?` | - | `` | Query integration time | + +**Range Values**: `AUTO`, `0.1`, `1`, `10`, `100`, `1000` + +**Example Session**: +``` +> *IDN? +< ThermalATE,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_/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** diff --git a/docs/03_architecture_decisions.md b/docs/03_architecture_decisions.md new file mode 100644 index 0000000..cee7f09 --- /dev/null +++ b/docs/03_architecture_decisions.md @@ -0,0 +1,189 @@ +# Architecture Decision Record +## ThermalATE: Technical Architecture Decisions + +| Document ID | ARD-001 | +|-------------|---------| +| Version | 1.1.0 | +| Status | Draft | +| Author | Kai Chappell | +| Created | 2025-12-01 | +| Last Updated | 2025-12-01 | + +--- + +## Purpose + +This document captures the **rationale** behind architectural decisions. It explains **why** certain approaches were chosen over alternatives. + +For **what** the system must do, see `01_requirements.md`. +For **how** to implement the system, see `02_technical_specification.md`. + +--- + +## Related Documents + +| Document | Purpose | +|----------|---------| +| `01_requirements.md` | Defines **what** the system must do | +| `02_technical_specification.md` | Specifies **how** to implement the system | +| `03_architecture_decisions.md` | Explains **why** decisions were made (this document) | + +--- + +## Table of Contents + +1. [Guiding Principles](#1-guiding-principles) +2. [Decision Log](#2-decision-log) + +--- + +## 1. Guiding Principles + +These principles guided all architectural decisions: + +| Principle | Rationale | +|-----------|-----------| +| **Single Responsibility** | Reduces coupling; each module changes for one reason only | +| **Dependency Inversion** | Enables testing and hardware swapping without code changes | +| **Explicit Dependencies** | Makes code predictable; no hidden global state | +| **Fail Fast** | Problems surface immediately rather than propagating | +| **Configuration over Code** | Runtime behaviour changes without recompilation | +| **Engineering Efficiency** | Prioritise development velocity using appropriate tools | + +--- + +## 2. Decision Log + +### ADR-001: Programming Language + +| Aspect | Content | +|--------|---------| +| **Context** | Need to choose primary implementation language for a DVT automation platform | +| **Decision** | Python 3.11+ | +| **Why This Choice** | Python is the industry standard for test automation. Hiring managers for DVT roles expect Python proficiency. The ecosystem (NumPy, SciPy, PyVISA) directly supports the problem domain. A single language reduces context-switching and demonstrates depth over breadth. | +| **Trade-offs Accepted** | Slower execution than compiled languages (acceptable for 100Hz simulation). Dynamic typing requires discipline (mitigated with type hints and mypy). | +| **Alternatives Rejected** | LabVIEW (proprietary, not demonstrable in portfolio), C++ (slower development velocity), TypeScript (not standard in ATE domain) | + +--- + +### ADR-002: Frontend Approach + +| Aspect | Content | +|--------|---------| +| **Context** | Need user interface for test execution and monitoring. Must balance effort against target role (DVT/Backend, not Frontend). Must demonstrate engineering efficiency. | +| **Decision** | CLI-first with Streamlit dashboard | +| **Why This Choice** | CLI demonstrates core functionality without UI overhead. Streamlit enables rapid iteration on dashboard UI using pure Python—no JavaScript, CSS, or complex state management required. This demonstrates 'Engineering Efficiency': building effective internal tools quickly using appropriate technology. Real ATE tools at hardware companies are often CLI or simple Python GUIs, not React SPAs. | +| **Trade-offs Accepted** | Streamlit has limitations for complex interactivity. Less customisable than React. Re-runs script on interaction (mitigated with caching). | +| **Alternatives Rejected** | React (wrong skill signal for DVT roles, major time investment, JavaScript required), Panel (more powerful but steeper learning curve, slower iteration), PyQt (higher complexity, dated appearance) | + +--- + +### ADR-003: Data Persistence Strategy + +| Aspect | Content | +|--------|---------| +| **Context** | Need to persist test metadata and high-frequency measurement data | +| **Decision** | SQLite for metadata + Parquet for time-series measurements | +| **Why This Choice** | SQLite requires no server process—the entire system remains self-contained. Parquet is columnar and optimised for analytical queries on measurement data. Both are file-based, making projects portable and easy to share. Standard library (sqlite3) and well-supported (PyArrow) tools demonstrate effective use of established technologies before reaching for external dependencies. | +| **Trade-offs Accepted** | Single-user only (acceptable for portfolio project). No real-time replication. Query capabilities limited to pandas/SQL without additional tools. | +| **Alternatives Rejected** | PostgreSQL (requires server, adds infrastructure complexity), InfluxDB (another service to manage), DuckDB (additional dependency not strictly necessary for single-user simulator) | + +--- + +### ADR-004: Instrument Communication Protocol + +| Aspect | Content | +|--------|---------| +| **Context** | Need protocol for test application to communicate with simulated instruments | +| **Decision** | SCPI over TCP/IP sockets | +| **Why This Choice** | SCPI is the industry standard for programmable instruments (IEEE 488.2). Using the real protocol means the same drivers work with real hardware. TCP/IP enables process separation and future network deployment. Demonstrates domain knowledge to reviewers. | +| **Trade-offs Accepted** | Must implement SCPI parser. Network overhead (negligible on localhost). | +| **Alternatives Rejected** | Direct function calls (wouldn't demonstrate real-world architecture), gRPC (not industry standard for instruments), REST API (not SCPI-compliant, wouldn't transfer to real hardware) | + +--- + +### ADR-005: Process Architecture + +| Aspect | Content | +|--------|---------| +| **Context** | Need to decide if physics simulation runs in-process or separately | +| **Decision** | Simulation server runs as separate process | +| **Why This Choice** | Mimics real hardware architecture where instruments are separate devices. Forces clean network boundaries that translate to real deployments. Enables Docker containerisation with proper service separation. The same test code works whether talking to simulation or real instruments over network. | +| **Trade-offs Accepted** | Process management complexity. Inter-process communication overhead. | +| **Alternatives Rejected** | In-process simulation (wouldn't demonstrate production architecture, wouldn't prove hardware abstraction works) | + +--- + +### ADR-006: Hardware Abstraction Mechanism + +| Aspect | Content | +|--------|---------| +| **Context** | Need to swap between simulated and real instruments without changing test code | +| **Decision** | Python Protocol classes (structural typing) with Factory pattern | +| **Why This Choice** | Protocols enable interface-based programming without inheritance coupling. Factory centralises the simulator-vs-real decision to configuration. No heavy DI framework needed—Python's simplicity is sufficient. Demonstrates understanding of SOLID principles and dependency injection. | +| **Trade-offs Accepted** | Manual wiring in factory (acceptable at this scale). Protocols require Python 3.8+. | +| **Alternatives Rejected** | Abstract Base Classes only (less flexible than Protocols), DI framework like dependency-injector (over-engineering for this scale), No abstraction (would couple test code to simulator) | + +--- + +### ADR-007: Concurrency Model + +| Aspect | Content | +|--------|---------| +| **Context** | Need to handle concurrent instrument connections and real-time physics updates | +| **Decision** | asyncio for simulation server; synchronous-first for test code | +| **Why This Choice** | Server must handle multiple simultaneous connections efficiently—asyncio is Python's standard solution. Test code is inherently sequential (set voltage, wait, measure), so forcing async everywhere adds complexity without benefit. Clear boundary at transport layer. | +| **Trade-offs Accepted** | Two patterns to understand. Must be careful about blocking calls in async context. | +| **Alternatives Rejected** | Threading everywhere (GIL limitations, harder to reason about), Full async in test code (unnecessary complexity, test sequences are sequential) | + +--- + +### ADR-008: Configuration Format + +| Aspect | Content | +|--------|---------| +| **Context** | Need human-readable, version-controllable configuration | +| **Decision** | YAML files validated with Pydantic | +| **Why This Choice** | YAML supports comments (unlike JSON), making configs self-documenting. Pydantic provides type-safe parsing with clear error messages. Configs can be version-controlled alongside code. | +| **Trade-offs Accepted** | YAML has some parsing quirks (Norway problem). Pydantic v2 has breaking changes from v1. | +| **Alternatives Rejected** | JSON (no comments), TOML (less common in Python data ecosystem), Environment variables only (not structured enough for complex config) | + +--- + +### ADR-009: Testing Philosophy + +| Aspect | Content | +|--------|---------| +| **Context** | Need automated testing strategy for quality assurance | +| **Decision** | pytest with dependency injection via fixtures; 80% coverage target on core modules | +| **Why This Choice** | pytest is the Python community standard. Fixtures align with DI pattern—inject mocks at test boundaries. 80% coverage balances thoroughness with pragmatism. Demonstrates professional engineering practices expected at senior level. | +| **Trade-offs Accepted** | Must design for testability from the start. Some integration tests require running simulation server. | +| **Alternatives Rejected** | No tests (unacceptable for portfolio demonstrating senior-level work), unittest (less ergonomic than pytest), 100% coverage target (diminishing returns, encourages testing trivial code) | + +--- + +### ADR-010: Project Structure + +| Aspect | Content | +|--------|---------| +| **Context** | Need to organise code for clarity and maintainability | +| **Decision** | Monorepo with src layout and package-per-concern | +| **Why This Choice** | Single repository simplifies development for a solo developer. Src layout is Python packaging best practice (prevents import confusion). Package-per-concern makes dependencies explicit and enforceable. | +| **Trade-offs Accepted** | Must configure editable installs for development. All code in one repo (acceptable at this scale). | +| **Alternatives Rejected** | Flat structure (poor for larger codebases), Multi-repo (overhead for single developer), Package-per-layer only (too coarse-grained) | + +--- + +### ADR-011: Development Phasing Strategy + +| Aspect | Content | +|--------|---------| +| **Context** | Need to prioritise features for efficient delivery of portfolio-quality project | +| **Decision** | Vertical Slice approach: deliver end-to-end functionality (Physics → HAL → Driver → UI) before adding horizontal features (reporting, API) | +| **Why This Choice** | A working "Virtual Lab Bench" demonstrates the core value proposition immediately. Vertical slices reduce integration risk—problems surface early. Defers low-value work (PDF formatting) until core is proven. Enables meaningful demos at every phase. | +| **Trade-offs Accepted** | Some infrastructure (reporting) delayed. May need refactoring when adding phases. | +| **Alternatives Rejected** | Horizontal layers first (delays integration, hides problems), Feature-complete phases (too long between demos), MVP without UI (harder to demonstrate value) | + +--- + +**End of Architecture Decision Record**