Compare commits
16 Commits
v0.0.1
...
v0.1.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c0d2ead54 | |||
| 2b78a75f51 | |||
| 15c9033153 | |||
| 0ab1181ec4 | |||
| bb3129e69b | |||
| 14858a087c | |||
| 7ecdbe007a | |||
| 568d1a6ca4 | |||
| 3db4969e44 | |||
| 8ef8c18e50 | |||
| eb13bb5bc4 | |||
| ca4613e318 | |||
| 7ca31c9c97 | |||
| 13d53d13df | |||
| 6a937876a3 | |||
| 85024f8670 |
35
CHANGELOG.md
35
CHANGELOG.md
@@ -7,6 +7,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [0.1.0-alpha.2] - 2025-12-02
|
||||
|
||||
### Added
|
||||
- Streamlit dashboard for interactive physics visualisation
|
||||
- Real-time temperature charts (chamber, case, junction)
|
||||
- Current state metrics display (voltages, currents, power, temperatures)
|
||||
- Interactive controls in sidebar:
|
||||
- Temperature setpoint slider (-40°C to 125°C)
|
||||
- Input voltage slider (0-12V)
|
||||
- Load current slider (0-500mA)
|
||||
- Output enable toggle
|
||||
- Start/Stop/Reset simulation buttons
|
||||
- Self-heating demonstration panel with:
|
||||
- Junction-case and case-ambient temperature rise display
|
||||
- Power dissipation chart
|
||||
- Thermal coupling explanation
|
||||
|
||||
## [0.1.0-alpha.1] - 2025-12-02
|
||||
|
||||
### Added
|
||||
- Physics engine with thermal-electrical coupling
|
||||
- First-order thermal response calculations for chamber and case
|
||||
- Junction temperature calculation via thermal resistance (θ_jc)
|
||||
- Self-heating effects from power dissipation
|
||||
- LDO DUT model with temperature-dependent behaviour
|
||||
- Output voltage temperature coefficient (ppm/°C)
|
||||
- Quiescent current temperature coefficient
|
||||
- Dropout voltage temperature dependence
|
||||
- Power dissipation calculation
|
||||
- Comprehensive physics engine test suite (13 tests)
|
||||
|
||||
## [0.0.1] - 2025-12-01
|
||||
|
||||
### Added
|
||||
@@ -29,6 +60,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
| 0.1.0-beta.2 | TBD | First DVT test runs |
|
||||
| 0.1.0-beta.1 | TBD | HAL complete |
|
||||
| 0.1.0-alpha.3 | TBD | Network ready |
|
||||
| 0.1.0-alpha.2 | TBD | Visual demo |
|
||||
| 0.1.0-alpha.1 | TBD | Physics engine |
|
||||
| 0.1.0-alpha.2 | 2025-12-02 | Visual demo |
|
||||
| 0.1.0-alpha.1 | 2025-12-02 | Physics engine |
|
||||
| 0.0.1 | 2025-12-01 | Project scaffolding |
|
||||
|
||||
@@ -214,7 +214,7 @@ For **why** decisions were made, see `03_architecture_decisions.md`.
|
||||
### 2.1 Directory Layout
|
||||
|
||||
```
|
||||
thermaulate/
|
||||
py_dvt_ate/
|
||||
├── pyproject.toml # Project metadata and dependencies
|
||||
├── README.md # Project overview and quick start
|
||||
├── CHANGELOG.md # Version history
|
||||
@@ -222,205 +222,163 @@ thermaulate/
|
||||
├── docs/
|
||||
│ ├── 01_requirements.md # Business Requirements
|
||||
│ ├── 02_technical_specification.md # Technical Design (this doc)
|
||||
│ └── 03_architecture_decisions.md # Architecture Decisions
|
||||
│ ├── 03_architecture_decisions.md # Architecture Decisions
|
||||
│ └── 04_development_plan.md # Sprint breakdown
|
||||
│
|
||||
├── src/
|
||||
│ └── thermaulate/
|
||||
├── src/py_dvt_ate/
|
||||
│ ├── __init__.py # Package version
|
||||
│ ├── py.typed # PEP 561 marker
|
||||
│ │
|
||||
│ ├── instruments/ # INSTRUMENT CONTROL (reusable)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── interfaces.py # IThermalChamber, IPowerSupply, IMultimeter
|
||||
│ │ ├── scpi.py # SCPI parser (shared protocol)
|
||||
│ │ ├── factory.py # Creates instrument sets from config
|
||||
│ │ ├── transport/ # Connection layer
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── base.py # Transport protocol
|
||||
│ │ │ ├── tcp.py # TCP socket transport
|
||||
│ │ │ └── visa.py # PyVISA transport (future)
|
||||
│ │ └── drivers/ # SCPI driver implementations
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py # Base driver
|
||||
│ │ ├── chamber.py # Thermal chamber driver
|
||||
│ │ ├── power_supply.py # PSU driver
|
||||
│ │ └── multimeter.py # DMM driver
|
||||
│ │
|
||||
│ ├── simulation/ # PHYSICS SIMULATION (dev/test only)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── server.py # TCP server hosting virtual instruments
|
||||
│ │ ├── physics/ # Physics engine
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── engine.py # Main simulation loop
|
||||
│ │ │ ├── thermal.py # Thermal calculations
|
||||
│ │ │ └── models/ # DUT models
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ ├── base.py # DUT protocol
|
||||
│ │ │ └── ldo.py # LDO model
|
||||
│ │ └── virtual/ # Virtual instrument implementations
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py # Base virtual instrument
|
||||
│ │ ├── chamber.py # Virtual thermal chamber
|
||||
│ │ ├── power_supply.py # Virtual PSU
|
||||
│ │ └── multimeter.py # Virtual DMM
|
||||
│ │
|
||||
│ ├── framework/ # TEST FRAMEWORK (reusable)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── runner.py # Test sequencer
|
||||
│ │ ├── context.py # Runtime context
|
||||
│ │ ├── logger.py # Measurement logging
|
||||
│ │ ├── limits.py # Pass/fail evaluation
|
||||
│ │ └── models.py # Framework models
|
||||
│ │
|
||||
│ ├── tests/ # DVT TEST IMPLEMENTATIONS
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── base.py # Base test class
|
||||
│ │ ├── thermal/ # Thermal characterisation tests
|
||||
│ │ │ ├── __init__.py
|
||||
│ │ │ └── tempco.py # Temperature coefficient test
|
||||
│ │ └── electrical/ # Electrical characterisation tests
|
||||
│ │ ├── __init__.py
|
||||
│ │ └── load_regulation.py # Load regulation test
|
||||
│ │
|
||||
│ ├── data/ # DATA PERSISTENCE (shared)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── repository.py # Data access layer
|
||||
│ │ └── models.py # Data models
|
||||
│ │
|
||||
│ ├── reporting/ # REPORT GENERATION (standalone)
|
||||
│ │ ├── __init__.py
|
||||
│ │ ├── generator.py # Report generator
|
||||
│ │ └── templates/ # Report templates
|
||||
│ │
|
||||
│ └── app/ # APPLICATION ENTRY POINTS
|
||||
│ ├── __init__.py
|
||||
│ ├── 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
|
||||
│ ├── cli.py # Command-line interface
|
||||
│ ├── config.py # YAML loading
|
||||
│ └── dashboard/ # Streamlit dashboard
|
||||
│ ├── __init__.py
|
||||
│ ├── models.py # Pydantic config models
|
||||
│ └── loader.py # Config file loader
|
||||
│ └── app.py # Main Streamlit app
|
||||
│
|
||||
├── 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
|
||||
│ └── ...
|
||||
├── tests/ # pytest test suite
|
||||
│ ├── conftest.py # pytest fixtures
|
||||
│ ├── unit/ # Unit tests
|
||||
│ └── integration/ # Integration tests
|
||||
│
|
||||
├── config/ # Configuration files
|
||||
│ ├── default.yaml # Default configuration
|
||||
│ └── example_pyvisa.yaml # Example for real hardware
|
||||
├── config/ # Configuration files
|
||||
│ └── default.yaml # Default configuration
|
||||
│
|
||||
├── 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
|
||||
└── docker/ # Docker deployment
|
||||
├── Dockerfile.server # Simulation server image
|
||||
├── Dockerfile.app # Test application image
|
||||
└── docker-compose.yml # Full stack orchestration
|
||||
```
|
||||
|
||||
### 2.2 Package Dependencies
|
||||
|
||||
```
|
||||
thermaulate/
|
||||
├── cli/ ──────────────────────────────────────────────┐
|
||||
├── api/ ──────────────────────────────────────────────┤
|
||||
├── dashboard/ ──────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ PRESENTATION │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
├── executive/ ◄───────────────────────────────────────────────┤
|
||||
├── tests/ ◄───────────────────────────────────────────────┤
|
||||
├── reporting/ ◄───────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ APPLICATION │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
├── hal/interfaces ◄───────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ DOMAIN (Abstractions) │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ implements│ │
|
||||
│ ▼ │
|
||||
├── hal/impl ◄───────────────────────────────────────────────┤
|
||||
├── drivers/ ◄───────────────────────────────────────────────┤
|
||||
├── transport/ ◄───────────────────────────────────────────────┤
|
||||
├── data/ ◄───────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────────────────────────────────────────┐ │
|
||||
│ │ INFRASTRUCTURE │ │
|
||||
│ └─────────────────────────────────────────────────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
Dependency Graph:
|
||||
|
||||
SIMULATION SERVER (Separate Process):
|
||||
├── physics/ ◄─── Pure domain logic, no external dependencies
|
||||
├── instruments/ ◄─── Depends on physics
|
||||
└── server/ ◄─── Depends on instruments
|
||||
app/ ──────────────▶ framework/ ──────────────▶ instruments/
|
||||
│ │ │
|
||||
│ ▼ │
|
||||
│ data/ ◀────────────────────────┘
|
||||
│ ▲
|
||||
▼ │
|
||||
reporting/ ──────────────┘
|
||||
|
||||
simulation/ ─────────────────────────────────▶ instruments/
|
||||
|
||||
Key:
|
||||
- app/ : CLI, dashboard, config loading (PRESENTATION)
|
||||
- framework/ : Test runner, logger, limits (APPLICATION)
|
||||
- instruments/ : Interfaces, drivers, transport, SCPI (DOMAIN)
|
||||
- data/ : Persistence layer (INFRASTRUCTURE)
|
||||
- reporting/ : Report generation (standalone)
|
||||
- simulation/ : Physics engine, virtual instruments (DEVELOPMENT)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Module Specifications
|
||||
|
||||
### 3.1 Physics Module
|
||||
### 3.1 Instruments Package
|
||||
|
||||
**Responsibility**: Simulate coupled thermal-electrical behaviour.
|
||||
**Responsibility**: Everything about talking to lab instruments.
|
||||
|
||||
**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 |
|
||||
| Interfaces | `instruments/interfaces.py` | IThermalChamber, IPowerSupply, IMultimeter protocols |
|
||||
| SCPIParser | `instruments/scpi.py` | Parse SCPI command strings |
|
||||
| Factory | `instruments/factory.py` | Create instrument sets from config |
|
||||
| Transport | `instruments/transport/` | TCP, VISA connection layer |
|
||||
| Drivers | `instruments/drivers/` | SCPI command implementations |
|
||||
|
||||
**Command Processing Flow**:
|
||||
```
|
||||
High-level call → Driver → SCPI command → Transport → Instrument
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Simulation Package
|
||||
|
||||
**Responsibility**: Physics simulation for development without real hardware.
|
||||
|
||||
**Key Components**:
|
||||
|
||||
| Component | File | Purpose |
|
||||
|-----------|------|---------|
|
||||
| Server | `simulation/server.py` | TCP server hosting virtual instruments |
|
||||
| PhysicsEngine | `simulation/physics/engine.py` | Main simulation loop |
|
||||
| ThermalModel | `simulation/physics/thermal.py` | Heat transfer calculations |
|
||||
| DUTBase | `simulation/physics/models/base.py` | Abstract DUT interface |
|
||||
| LDOModel | `simulation/physics/models/ldo.py` | LDO voltage regulator model |
|
||||
| VirtualChamber | `simulation/virtual/chamber.py` | Virtual thermal chamber |
|
||||
| VirtualPSU | `simulation/virtual/power_supply.py` | Virtual power supply |
|
||||
| VirtualDMM | `simulation/virtual/multimeter.py` | Virtual multimeter |
|
||||
|
||||
**State Management**:
|
||||
- Engine maintains global simulation time
|
||||
@@ -429,108 +387,68 @@ SIMULATION SERVER (Separate Process):
|
||||
|
||||
---
|
||||
|
||||
### 3.2 Instruments Module
|
||||
### 3.3 Framework Package
|
||||
|
||||
**Responsibility**: SCPI-compliant virtual instrument behaviour.
|
||||
**Responsibility**: Test execution infrastructure.
|
||||
|
||||
**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
|
||||
```
|
||||
| TestRunner | `framework/runner.py` | Sequences test steps |
|
||||
| TestContext | `framework/context.py` | Runtime context |
|
||||
| TestLogger | `framework/logger.py` | Measurement logging |
|
||||
| LimitChecker | `framework/limits.py` | Pass/fail evaluation |
|
||||
| Models | `framework/models.py` | TestStatus, TestResult, etc. |
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Transport Module
|
||||
### 3.4 Data Package
|
||||
|
||||
**Responsibility**: Low-level communication.
|
||||
**Responsibility**: Data persistence for test results.
|
||||
|
||||
**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 |
|
||||
| Repository | `data/repository.py` | Data access layer |
|
||||
| Models | `data/models.py` | TestRun, Measurement dataclasses |
|
||||
|
||||
---
|
||||
|
||||
### 3.4 Drivers Module
|
||||
### 3.5 Reporting Package
|
||||
|
||||
**Responsibility**: Instrument-specific SCPI command sets.
|
||||
**Responsibility**: Report generation from stored data.
|
||||
|
||||
**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 |
|
||||
| Generator | `reporting/generator.py` | Creates reports from data |
|
||||
| Templates | `reporting/templates/` | Report templates |
|
||||
|
||||
---
|
||||
|
||||
### 3.5 HAL Module
|
||||
### 3.6 App Package
|
||||
|
||||
**Responsibility**: Hardware abstraction interfaces.
|
||||
**Responsibility**: Application entry points.
|
||||
|
||||
**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 |
|
||||
| CLI | `app/cli.py` | Command-line interface (Typer) |
|
||||
| Config | `app/config.py` | YAML loading, instance creation |
|
||||
| Dashboard | `app/dashboard/app.py` | Streamlit application |
|
||||
|
||||
---
|
||||
|
||||
## 4. Interface Definitions
|
||||
|
||||
### 4.1 HAL Interfaces
|
||||
### 4.1 Instrument Interfaces
|
||||
|
||||
```python
|
||||
# thermaulate/hal/interfaces.py
|
||||
# py_dvt_ate/instruments/interfaces.py
|
||||
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
@@ -664,7 +582,7 @@ class ITestLogger(Protocol):
|
||||
### 4.2 Transport Interface
|
||||
|
||||
```python
|
||||
# thermaulate/transport/base.py
|
||||
# py_dvt_ate/instruments/transport/base.py
|
||||
|
||||
from typing import Protocol
|
||||
|
||||
@@ -701,7 +619,7 @@ class Transport(Protocol):
|
||||
### 4.3 Test Interface
|
||||
|
||||
```python
|
||||
# thermaulate/executive/models.py
|
||||
# py_dvt_ate/framework/models.py
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
@@ -778,12 +696,12 @@ class ITest(Protocol):
|
||||
### 4.4 Factory Interface
|
||||
|
||||
```python
|
||||
# thermaulate/hal/factory.py
|
||||
# py_dvt_ate/instruments/factory.py
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Literal
|
||||
|
||||
from thermaulate.hal.interfaces import IThermalChamber, IPowerSupply, IMultimeter
|
||||
from py_dvt_ate.instruments.interfaces import IThermalChamber, IPowerSupply, IMultimeter
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -827,22 +745,19 @@ class InstrumentFactory:
|
||||
@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
|
||||
from py_dvt_ate.instruments.transport.tcp import TCPTransport
|
||||
from py_dvt_ate.instruments.drivers.chamber import ThermalChamberDriver
|
||||
from py_dvt_ate.instruments.drivers.power_supply import PowerSupplyDriver
|
||||
from py_dvt_ate.instruments.drivers.multimeter import MultimeterDriver
|
||||
|
||||
chamber_transport = TCPTransport(config.simulator_host, config.chamber_port)
|
||||
psu_transport = TCPTransport(config.simulator_host, config.psu_port)
|
||||
dmm_transport = TCPTransport(config.simulator_host, config.dmm_port)
|
||||
|
||||
return InstrumentSet(
|
||||
chamber=ThermalChamberHAL(ThermalChamberDriver(chamber_transport)),
|
||||
psu=PowerSupplyHAL(PowerSupplyDriver(psu_transport)),
|
||||
dmm=MultimeterHAL(MultimeterDriver(dmm_transport)),
|
||||
chamber=ThermalChamberDriver(chamber_transport),
|
||||
psu=PowerSupplyDriver(psu_transport),
|
||||
dmm=MultimeterDriver(dmm_transport),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@@ -954,7 +869,7 @@ All instruments implement these standard commands:
|
||||
### 5.5 SCPI Parser Specification
|
||||
|
||||
```python
|
||||
# thermaulate/instruments/scpi_parser.py
|
||||
# py_dvt_ate/instruments/scpi.py
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -1071,7 +986,7 @@ P_diss = (V_in - V_out) × I_load + V_in × I_q
|
||||
### 6.3 Physics Engine Implementation
|
||||
|
||||
```python
|
||||
# thermaulate/physics/engine.py
|
||||
# py_dvt_ate/simulation/physics/engine.py
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
@@ -1256,7 +1171,7 @@ Schema:
|
||||
### 7.3 Data Repository Interface
|
||||
|
||||
```python
|
||||
# thermaulate/data/repository.py
|
||||
# py_dvt_ate/data/repository.py (interface)
|
||||
|
||||
from typing import Protocol
|
||||
from uuid import UUID
|
||||
@@ -1366,14 +1281,14 @@ dut:
|
||||
|
||||
# Data storage paths
|
||||
data:
|
||||
database_path: "./data/thermaulate.db"
|
||||
database_path: "./data/py_dvt_ate.db"
|
||||
measurements_dir: "./data/measurements"
|
||||
reports_dir: "./data/reports"
|
||||
|
||||
# Logging configuration
|
||||
logging:
|
||||
level: INFO
|
||||
file: "./data/logs/thermaulate.log"
|
||||
file: "./data/logs/py_dvt_ate.log"
|
||||
format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
# Dashboard (Streamlit)
|
||||
@@ -1391,7 +1306,7 @@ api:
|
||||
### 8.2 Pydantic Configuration Models
|
||||
|
||||
```python
|
||||
# thermaulate/config/models.py
|
||||
# py_dvt_ate/app/config.py (config models)
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Literal
|
||||
@@ -1449,14 +1364,14 @@ class DUTConfig(BaseModel):
|
||||
|
||||
|
||||
class DataConfig(BaseModel):
|
||||
database_path: str = "./data/thermaulate.db"
|
||||
database_path: str = "./data/py_dvt_ate.db"
|
||||
measurements_dir: str = "./data/measurements"
|
||||
reports_dir: str = "./data/reports"
|
||||
|
||||
|
||||
class LoggingConfig(BaseModel):
|
||||
level: str = "INFO"
|
||||
file: str = "./data/logs/thermaulate.log"
|
||||
file: str = "./data/logs/py_dvt_ate.log"
|
||||
format: str = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
|
||||
|
||||
@@ -1612,7 +1527,7 @@ class AppConfig(BaseModel):
|
||||
|
||||
```toml
|
||||
[project]
|
||||
name = "thermaulate"
|
||||
name = "py_dvt_ate"
|
||||
version = "0.1.0"
|
||||
description = "Coupled Physics DVT Simulation Platform"
|
||||
requires-python = ">=3.11"
|
||||
@@ -1648,9 +1563,9 @@ dev = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
thermaulate = "thermaulate.cli.main:app"
|
||||
thermaulate-server = "thermaulate.server.main:main"
|
||||
thermaulate-dashboard = "thermaulate.dashboard.app:main"
|
||||
py_dvt_ate = "py_dvt_ate.cli.main:app"
|
||||
py_dvt_ate-server = "py_dvt_ate.server.main:main"
|
||||
py_dvt_ate-dashboard = "py_dvt_ate.dashboard.app:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
|
||||
@@ -38,7 +38,7 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Vertical Slice Strategy:**
|
||||
- Sprints 1-3: Foundation + Physics Engine (the core simulation)
|
||||
- Sprint 4: Dashboard (see the physics working!)
|
||||
- Sprints 5-11: Infrastructure/Plumbing (SCPI, TCP, HAL)
|
||||
- Sprints 5-11: Infrastructure/Plumbing (SCPI, TCP, Instruments)
|
||||
- Sprints 12-17: Test Framework, CLI, Polish
|
||||
|
||||
---
|
||||
@@ -79,20 +79,18 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Define physics interfaces and data structures.
|
||||
|
||||
### Task 2.1: Define thermal state dataclasses
|
||||
- Create src/py_dvt_ate/physics/__init__.py
|
||||
- Create src/py_dvt_ate/physics/models.py
|
||||
- Create src/py_dvt_ate/simulation/physics/models.py
|
||||
- Define ThermalState (frozen dataclass)
|
||||
- Define ElectricalState (frozen dataclass)
|
||||
- **Commit:** "Add physics state dataclasses"
|
||||
|
||||
### Task 2.2: Define DUT base protocol
|
||||
- Create src/py_dvt_ate/physics/dut/__init__.py
|
||||
- Create src/py_dvt_ate/physics/dut/base.py
|
||||
- Create src/py_dvt_ate/simulation/physics/models/base.py
|
||||
- Define DUTModel Protocol with method signatures
|
||||
- **Commit:** "Add DUT model protocol"
|
||||
|
||||
### Task 2.3: Create physics engine stub
|
||||
- Create src/py_dvt_ate/physics/engine.py
|
||||
- Create src/py_dvt_ate/simulation/physics/engine.py
|
||||
- Define PhysicsEngine class with stub methods
|
||||
- Methods return placeholder values
|
||||
- **Commit:** "Add physics engine stub"
|
||||
@@ -109,13 +107,13 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Implement working physics simulation.
|
||||
|
||||
### Task 3.1: Implement thermal calculations
|
||||
- Create src/py_dvt_ate/physics/thermal.py
|
||||
- Create src/py_dvt_ate/simulation/physics/thermal.py
|
||||
- Implement first-order thermal response calculations
|
||||
- Pure functions, no state
|
||||
- **Commit:** "Implement thermal calculation functions"
|
||||
|
||||
### Task 3.2: Implement LDO DUT model
|
||||
- Create src/py_dvt_ate/physics/dut/ldo.py
|
||||
- Create src/py_dvt_ate/simulation/physics/models/ldo.py
|
||||
- Implement LDOModel class
|
||||
- Temperature-dependent Vout, Iq calculations
|
||||
- Power dissipation calculation
|
||||
@@ -140,8 +138,8 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Visualise physics engine directly - see something working!
|
||||
|
||||
### Task 4.1: Create dashboard app skeleton
|
||||
- Create src/py_dvt_ate/dashboard/__init__.py
|
||||
- Create src/py_dvt_ate/dashboard/app.py
|
||||
- Create src/py_dvt_ate/app/dashboard/__init__.py
|
||||
- Create src/py_dvt_ate/app/dashboard/app.py
|
||||
- Basic Streamlit page with title
|
||||
- **Commit:** "Add Streamlit dashboard skeleton"
|
||||
|
||||
@@ -173,7 +171,7 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
|
||||
### Task 5.1: Define SCPI command dataclass
|
||||
- Create src/py_dvt_ate/instruments/__init__.py
|
||||
- Create src/py_dvt_ate/instruments/scpi_parser.py
|
||||
- Create src/py_dvt_ate/instruments/scpi.py
|
||||
- Define SCPICommand dataclass
|
||||
- **Commit:** "Add SCPI command dataclass"
|
||||
|
||||
@@ -196,13 +194,13 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Create first virtual instrument.
|
||||
|
||||
### Task 6.1: Define instrument base class
|
||||
- Create src/py_dvt_ate/instruments/base.py
|
||||
- Create src/py_dvt_ate/simulation/virtual/base.py
|
||||
- Define BaseInstrument with common functionality
|
||||
- Command dispatch mechanism
|
||||
- **Commit:** "Add base instrument class"
|
||||
|
||||
### Task 6.2: Create thermal chamber simulator stub
|
||||
- Create src/py_dvt_ate/instruments/thermal_chamber.py
|
||||
- Create src/py_dvt_ate/simulation/virtual/chamber.py
|
||||
- Define ThermalChamberSim class
|
||||
- Stub SCPI command handlers
|
||||
- **Commit:** "Add thermal chamber simulator stub"
|
||||
@@ -224,7 +222,7 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Complete instrument simulators.
|
||||
|
||||
### Task 7.1: Create power supply simulator
|
||||
- Create src/py_dvt_ate/instruments/power_supply.py
|
||||
- Create src/py_dvt_ate/simulation/virtual/power_supply.py
|
||||
- Implement PSU SCPI commands
|
||||
- VOLT, CURR, OUTP, MEAS commands
|
||||
- **Commit:** "Add power supply simulator"
|
||||
@@ -234,7 +232,7 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
- **Commit:** "Add power supply simulator tests"
|
||||
|
||||
### Task 7.3: Create DMM simulator
|
||||
- Create src/py_dvt_ate/instruments/multimeter.py
|
||||
- Create src/py_dvt_ate/simulation/virtual/multimeter.py
|
||||
- Implement DMM SCPI commands
|
||||
- MEAS:VOLT:DC?, CONF commands
|
||||
- **Commit:** "Add multimeter simulator"
|
||||
@@ -250,8 +248,8 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Expose instruments over network.
|
||||
|
||||
### Task 8.1: Create async TCP server foundation
|
||||
- Create src/py_dvt_ate/server/__init__.py
|
||||
- Create src/py_dvt_ate/server/tcp_server.py
|
||||
- Create src/py_dvt_ate/simulation/__init__.py
|
||||
- Create src/py_dvt_ate/simulation/tcp_server.py
|
||||
- Define InstrumentServer class with asyncio
|
||||
- **Commit:** "Add async TCP server foundation"
|
||||
|
||||
@@ -261,7 +259,7 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
- **Commit:** "Implement TCP client handling"
|
||||
|
||||
### Task 8.3: Create server main entry point
|
||||
- Create src/py_dvt_ate/server/main.py
|
||||
- Create src/py_dvt_ate/simulation/server.py
|
||||
- Wire up physics engine and instruments
|
||||
- Add CLI command to start server
|
||||
- **Commit:** "Add simulation server entry point"
|
||||
@@ -278,13 +276,13 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Create client-side communication.
|
||||
|
||||
### Task 9.1: Define transport protocol
|
||||
- Create src/py_dvt_ate/transport/__init__.py
|
||||
- Create src/py_dvt_ate/transport/base.py
|
||||
- Create src/py_dvt_ate/instruments/transport/__init__.py
|
||||
- Create src/py_dvt_ate/instruments/transport/base.py
|
||||
- Define Transport Protocol class
|
||||
- **Commit:** "Add transport protocol definition"
|
||||
|
||||
### Task 9.2: Implement TCP transport
|
||||
- Create src/py_dvt_ate/transport/tcp.py
|
||||
- Create src/py_dvt_ate/instruments/transport/tcp.py
|
||||
- Implement TCPTransport class
|
||||
- connect(), write(), read(), query() methods
|
||||
- **Commit:** "Implement TCP transport"
|
||||
@@ -301,19 +299,19 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Create instrument drivers using transport.
|
||||
|
||||
### Task 10.1: Define driver base class
|
||||
- Create src/py_dvt_ate/drivers/__init__.py
|
||||
- Create src/py_dvt_ate/drivers/base.py
|
||||
- Create src/py_dvt_ate/instruments/drivers/__init__.py
|
||||
- Create src/py_dvt_ate/instruments/drivers/base.py
|
||||
- Define BaseDriver with transport dependency
|
||||
- **Commit:** "Add driver base class"
|
||||
|
||||
### Task 10.2: Implement thermal chamber driver
|
||||
- Create src/py_dvt_ate/drivers/thermal_chamber.py
|
||||
- Create src/py_dvt_ate/instruments/drivers/chamber.py
|
||||
- Methods map to SCPI commands
|
||||
- **Commit:** "Add thermal chamber driver"
|
||||
|
||||
### Task 10.3: Implement PSU and DMM drivers
|
||||
- Create src/py_dvt_ate/drivers/power_supply.py
|
||||
- Create src/py_dvt_ate/drivers/multimeter.py
|
||||
- Create src/py_dvt_ate/instruments/drivers/power_supply.py
|
||||
- Create src/py_dvt_ate/instruments/drivers/multimeter.py
|
||||
- **Commit:** "Add PSU and DMM drivers"
|
||||
|
||||
### Task 10.4: Add driver tests
|
||||
@@ -323,32 +321,30 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
|
||||
---
|
||||
|
||||
## Sprint 11: Hardware Abstraction Layer
|
||||
## Sprint 11: Instrument Interfaces
|
||||
|
||||
**Goal:** Create HAL interfaces and implementations.
|
||||
**Goal:** Create instrument protocol interfaces and factory.
|
||||
|
||||
### Task 11.1: Define HAL protocols
|
||||
- Create src/py_dvt_ate/hal/__init__.py
|
||||
- Create src/py_dvt_ate/hal/interfaces.py
|
||||
- Define IThermalChamber, IPowerSupply, IMultimeter
|
||||
- **Commit:** "Add HAL protocol definitions"
|
||||
### Task 11.1: Define instrument interface protocols
|
||||
- Create src/py_dvt_ate/instruments/interfaces.py
|
||||
- Define IThermalChamber, IPowerSupply, IMultimeter protocols
|
||||
- **Commit:** "Add instrument interface protocols"
|
||||
|
||||
### Task 11.2: Implement HAL wrappers
|
||||
- Create src/py_dvt_ate/hal/impl/__init__.py
|
||||
- Create HAL implementation classes
|
||||
- Wrap drivers with HAL interface
|
||||
- **Commit:** "Add HAL implementations"
|
||||
### Task 11.2: Ensure drivers implement interfaces
|
||||
- Update drivers to satisfy Protocol interfaces
|
||||
- Add type hints for interface compliance
|
||||
- **Commit:** "Implement instrument interfaces in drivers"
|
||||
|
||||
### Task 11.3: Create instrument factory
|
||||
- Create src/py_dvt_ate/hal/factory.py
|
||||
- Create src/py_dvt_ate/instruments/factory.py
|
||||
- InstrumentSet dataclass
|
||||
- InstrumentFactory.create() method
|
||||
- **Commit:** "Add instrument factory"
|
||||
|
||||
### Task 11.4: Add HAL tests
|
||||
- Create tests/unit/test_hal.py
|
||||
### Task 11.4: Add instrument interface tests
|
||||
- Create tests/unit/test_instruments.py
|
||||
- Test factory creates correct types
|
||||
- **Commit:** "Add HAL unit tests"
|
||||
- **Commit:** "Add instrument interface tests"
|
||||
|
||||
---
|
||||
|
||||
@@ -357,13 +353,12 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** YAML-based configuration.
|
||||
|
||||
### Task 12.1: Define config models
|
||||
- Create src/py_dvt_ate/config/__init__.py
|
||||
- Create src/py_dvt_ate/config/models.py
|
||||
- Pydantic models for all config sections
|
||||
- Create src/py_dvt_ate/app/config.py
|
||||
- Define Pydantic models for all config sections
|
||||
- **Commit:** "Add configuration Pydantic models"
|
||||
|
||||
### Task 12.2: Implement config loader
|
||||
- Create src/py_dvt_ate/config/loader.py
|
||||
- Add load_config() function to src/py_dvt_ate/app/config.py
|
||||
- Load YAML, validate with Pydantic
|
||||
- Environment variable overrides
|
||||
- **Commit:** "Implement configuration loader"
|
||||
@@ -410,25 +405,25 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
**Goal:** Test execution orchestration.
|
||||
|
||||
### Task 14.1: Define test interface and models
|
||||
- Create src/py_dvt_ate/executive/__init__.py
|
||||
- Create src/py_dvt_ate/executive/models.py
|
||||
- Create src/py_dvt_ate/framework/__init__.py
|
||||
- Create src/py_dvt_ate/framework/context.py
|
||||
- TestStatus enum, TestContext, ITest protocol
|
||||
- **Commit:** "Add test executive models"
|
||||
- **Commit:** "Add test framework models"
|
||||
|
||||
### Task 14.2: Implement test logger
|
||||
- Create src/py_dvt_ate/executive/logger.py
|
||||
- Create src/py_dvt_ate/framework/logger.py
|
||||
- Log measurements and events
|
||||
- **Commit:** "Implement test logger"
|
||||
|
||||
### Task 14.3: Implement limit checker
|
||||
- Create src/py_dvt_ate/executive/limits.py
|
||||
- Create src/py_dvt_ate/framework/limits.py
|
||||
- Evaluate pass/fail against limits
|
||||
- **Commit:** "Implement limit checker"
|
||||
|
||||
### Task 14.4: Implement test sequencer
|
||||
- Create src/py_dvt_ate/executive/sequencer.py
|
||||
### Task 14.4: Implement test runner
|
||||
- Create src/py_dvt_ate/framework/runner.py
|
||||
- Run tests, collect results
|
||||
- **Commit:** "Implement test sequencer"
|
||||
- **Commit:** "Implement test runner"
|
||||
|
||||
---
|
||||
|
||||
@@ -443,7 +438,7 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
- **Commit:** "Add DVT test base class"
|
||||
|
||||
### Task 15.2: Implement TempCo test
|
||||
- Create src/py_dvt_ate/tests/tempco.py
|
||||
- Create src/py_dvt_ate/tests/thermal/tempco.py
|
||||
- Temperature sweep logic
|
||||
- Vout measurement at each temperature
|
||||
- TempCo calculation
|
||||
@@ -517,40 +512,38 @@ Each sprint is ~1-2 days of work, producing demonstrable progress.
|
||||
## File Dependencies Map
|
||||
|
||||
```
|
||||
physics/models.py → (none)
|
||||
physics/dut/base.py → models.py
|
||||
physics/dut/ldo.py → base.py, models.py
|
||||
physics/thermal.py → models.py
|
||||
physics/engine.py → models.py, thermal.py, dut/base.py
|
||||
simulation/physics/models.py → (none)
|
||||
simulation/physics/models/base.py → models.py
|
||||
simulation/physics/models/ldo.py → base.py, models.py
|
||||
simulation/physics/thermal.py → models.py
|
||||
simulation/physics/engine.py → models.py, thermal.py, models/base.py
|
||||
|
||||
dashboard/app.py → physics/engine.py (Sprint 4, direct connection)
|
||||
app/dashboard/app.py → simulation/physics/engine.py (Sprint 4)
|
||||
|
||||
instruments/scpi_parser.py → (none)
|
||||
instruments/base.py → scpi_parser.py
|
||||
instruments/*_sim.py → base.py, physics/engine.py
|
||||
instruments/scpi.py → (none)
|
||||
simulation/virtual/base.py → instruments/scpi.py
|
||||
simulation/virtual/*.py → base.py, simulation/physics/engine.py
|
||||
|
||||
transport/base.py → (none)
|
||||
transport/tcp.py → base.py
|
||||
instruments/transport/base.py → (none)
|
||||
instruments/transport/tcp.py → base.py
|
||||
|
||||
drivers/base.py → transport/base.py
|
||||
drivers/*.py → base.py
|
||||
instruments/drivers/base.py → instruments/transport/base.py
|
||||
instruments/drivers/*.py → base.py
|
||||
|
||||
hal/interfaces.py → (none)
|
||||
hal/impl/*.py → interfaces.py, drivers/*.py
|
||||
hal/factory.py → interfaces.py, impl/*.py
|
||||
instruments/interfaces.py → (none)
|
||||
instruments/factory.py → interfaces.py, drivers/*.py
|
||||
|
||||
config/models.py → (none)
|
||||
config/loader.py → models.py
|
||||
app/config.py → (none)
|
||||
|
||||
data/models.py → (none)
|
||||
data/repository.py → models.py
|
||||
data/models.py → (none)
|
||||
data/repository.py → models.py
|
||||
|
||||
executive/models.py → hal/interfaces.py
|
||||
executive/*.py → models.py, data/repository.py
|
||||
framework/context.py → instruments/interfaces.py
|
||||
framework/*.py → context.py, data/repository.py
|
||||
|
||||
tests/*.py → executive/models.py, hal/interfaces.py
|
||||
tests/*.py → framework/context.py, instruments/interfaces.py
|
||||
|
||||
dashboard/app.py → hal/factory.py (Sprint 17, upgraded)
|
||||
app/dashboard/app.py → instruments/factory.py (Sprint 17, upgraded)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -578,7 +571,7 @@ MAJOR.MINOR.PATCH[-PRERELEASE]
|
||||
| 3 | `v0.1.0-alpha.1` | Physics engine working | Pre-release |
|
||||
| 4 | `v0.1.0-alpha.2` | Visual demo (dashboard) | Pre-release |
|
||||
| 8 | `v0.1.0-alpha.3` | Network ready (TCP server) | Pre-release |
|
||||
| 11 | `v0.1.0-beta.1` | HAL complete | Pre-release |
|
||||
| 11 | `v0.1.0-beta.1` | Interfaces complete | Pre-release |
|
||||
| 15 | `v0.1.0-beta.2` | First DVT test runs | Pre-release |
|
||||
| 17 | `v0.1.0` | **MVP Complete** | Release |
|
||||
|
||||
@@ -644,7 +637,7 @@ Maintain `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/
|
||||
| 4 | `v0.1.0-alpha.2` | **Visual Demo!** | Interactive Streamlit showing physics |
|
||||
| 7 | - | Instruments Done | SCPI simulators respond to commands |
|
||||
| 8 | `v0.1.0-alpha.3` | Network Ready | TCP server accepts connections |
|
||||
| 11 | `v0.1.0-beta.1` | HAL Complete | Abstraction layer swappable |
|
||||
| 11 | `v0.1.0-beta.1` | Interfaces Complete | Instrument layer swappable |
|
||||
| 15 | `v0.1.0-beta.2` | First Test | TempCo characterisation runs |
|
||||
| 17 | `v0.1.0` | **MVP Complete** | Full end-to-end workflow |
|
||||
|
||||
|
||||
@@ -40,9 +40,9 @@ dev = [
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
py-dvt-ate = "py_dvt_ate.cli.main:app"
|
||||
py-dvt-ate-server = "py_dvt_ate.server.main:main"
|
||||
py-dvt-ate-dashboard = "py_dvt_ate.dashboard.app:main"
|
||||
py-dvt-ate = "py_dvt_ate.app.cli:app"
|
||||
py-dvt-ate-server = "py_dvt_ate.simulation.server:main"
|
||||
py-dvt-ate-dashboard = "py_dvt_ate.app.dashboard.app:main"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""py_dvt_ate: Coupled Physics DVT Simulation Platform."""
|
||||
|
||||
__version__ = "0.0.1"
|
||||
__version__ = "0.1.0-alpha.2"
|
||||
|
||||
5
src/py_dvt_ate/app/__init__.py
Normal file
5
src/py_dvt_ate/app/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Application entry points.
|
||||
|
||||
Contains CLI, dashboard, and configuration loading for the
|
||||
py_dvt_ate application.
|
||||
"""
|
||||
9
src/py_dvt_ate/app/dashboard/__init__.py
Normal file
9
src/py_dvt_ate/app/dashboard/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Streamlit dashboard for real-time monitoring.
|
||||
|
||||
Provides visualisation of instrument status, test progress,
|
||||
and historical results.
|
||||
"""
|
||||
|
||||
from py_dvt_ate.app.dashboard.app import main
|
||||
|
||||
__all__ = ["main"]
|
||||
298
src/py_dvt_ate/app/dashboard/app.py
Normal file
298
src/py_dvt_ate/app/dashboard/app.py
Normal file
@@ -0,0 +1,298 @@
|
||||
"""Streamlit dashboard application for physics simulation visualisation.
|
||||
|
||||
This module provides an interactive dashboard for visualising the physics
|
||||
engine directly, demonstrating thermal-electrical coupling in real-time.
|
||||
"""
|
||||
|
||||
from collections import deque
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from py_dvt_ate.simulation.physics.engine import PhysicsEngine
|
||||
|
||||
|
||||
# History buffer size for charts
|
||||
HISTORY_SIZE = 500
|
||||
|
||||
|
||||
@dataclass
|
||||
class SimulationHistory:
|
||||
"""Stores time series data for visualisation."""
|
||||
|
||||
time: deque[float] = field(default_factory=lambda: deque(maxlen=HISTORY_SIZE))
|
||||
chamber_temp: deque[float] = field(
|
||||
default_factory=lambda: deque(maxlen=HISTORY_SIZE)
|
||||
)
|
||||
case_temp: deque[float] = field(default_factory=lambda: deque(maxlen=HISTORY_SIZE))
|
||||
junction_temp: deque[float] = field(
|
||||
default_factory=lambda: deque(maxlen=HISTORY_SIZE)
|
||||
)
|
||||
output_voltage: deque[float] = field(
|
||||
default_factory=lambda: deque(maxlen=HISTORY_SIZE)
|
||||
)
|
||||
power_dissipation: deque[float] = field(
|
||||
default_factory=lambda: deque(maxlen=HISTORY_SIZE)
|
||||
)
|
||||
|
||||
|
||||
def init_session_state() -> None:
|
||||
"""Initialise Streamlit session state."""
|
||||
if "engine" not in st.session_state:
|
||||
st.session_state.engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
if "history" not in st.session_state:
|
||||
st.session_state.history = SimulationHistory()
|
||||
if "running" not in st.session_state:
|
||||
st.session_state.running = False
|
||||
|
||||
|
||||
def step_simulation(steps: int = 10) -> None:
|
||||
"""Advance the simulation by the given number of steps."""
|
||||
engine: PhysicsEngine = st.session_state.engine
|
||||
history: SimulationHistory = st.session_state.history
|
||||
|
||||
for _ in range(steps):
|
||||
engine.step()
|
||||
|
||||
# Record current state in history
|
||||
thermal = engine.get_thermal_state()
|
||||
electrical = engine.get_electrical_state()
|
||||
|
||||
history.time.append(thermal.timestamp)
|
||||
history.chamber_temp.append(thermal.chamber_temperature)
|
||||
history.case_temp.append(thermal.case_temperature)
|
||||
history.junction_temp.append(thermal.junction_temperature)
|
||||
history.output_voltage.append(electrical.output_voltage)
|
||||
history.power_dissipation.append(electrical.power_dissipation)
|
||||
|
||||
|
||||
def display_thermal_chart() -> None:
|
||||
"""Display temperature chart."""
|
||||
history: SimulationHistory = st.session_state.history
|
||||
|
||||
if len(history.time) < 2:
|
||||
st.info("Start the simulation to see temperature data")
|
||||
return
|
||||
|
||||
chart_data = {
|
||||
"Time (s)": list(history.time),
|
||||
"Chamber": list(history.chamber_temp),
|
||||
"Case": list(history.case_temp),
|
||||
"Junction": list(history.junction_temp),
|
||||
}
|
||||
|
||||
st.line_chart(
|
||||
chart_data,
|
||||
x="Time (s)",
|
||||
y=["Chamber", "Case", "Junction"],
|
||||
color=["#1f77b4", "#ff7f0e", "#d62728"],
|
||||
)
|
||||
|
||||
|
||||
def display_self_heating_panel() -> None:
|
||||
"""Display self-heating demonstration panel."""
|
||||
engine: PhysicsEngine = st.session_state.engine
|
||||
history: SimulationHistory = st.session_state.history
|
||||
|
||||
thermal = engine.get_thermal_state()
|
||||
electrical = engine.get_electrical_state()
|
||||
|
||||
# Calculate temperature rises
|
||||
delta_t_jc = thermal.junction_temperature - thermal.case_temperature
|
||||
delta_t_ca = thermal.case_temperature - thermal.chamber_temperature
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.markdown("#### Self-Heating Analysis")
|
||||
|
||||
# Display thermal resistance info
|
||||
st.markdown(
|
||||
f"""
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Junction-Case Rise (ΔT_jc) | **{delta_t_jc:.2f} °C** |
|
||||
| Case-Ambient Rise (ΔT_ca) | **{delta_t_ca:.2f} °C** |
|
||||
| Power Dissipation | {electrical.power_dissipation * 1000:.1f} mW |
|
||||
| θ_jc (junction-case) | 15 °C/W |
|
||||
| θ_ca (case-ambient) | 5 °C/W |
|
||||
"""
|
||||
)
|
||||
|
||||
st.markdown(
|
||||
"""
|
||||
**Thermal Coupling:** The junction temperature rises above the case
|
||||
temperature due to power dissipation. This is governed by:
|
||||
|
||||
`T_junction = T_case + P_diss × θ_jc`
|
||||
|
||||
Try increasing the load current or input voltage to see
|
||||
self-heating effects!
|
||||
"""
|
||||
)
|
||||
|
||||
with col2:
|
||||
st.markdown("#### Power Dissipation")
|
||||
|
||||
if len(history.time) < 2:
|
||||
st.info("Start the simulation to see power data")
|
||||
return
|
||||
|
||||
power_data = {
|
||||
"Time (s)": list(history.time),
|
||||
"Power (mW)": [p * 1000 for p in history.power_dissipation],
|
||||
}
|
||||
|
||||
st.line_chart(
|
||||
power_data,
|
||||
x="Time (s)",
|
||||
y="Power (mW)",
|
||||
color="#2ca02c",
|
||||
)
|
||||
|
||||
|
||||
def display_current_state() -> None:
|
||||
"""Display current simulation state metrics."""
|
||||
engine: PhysicsEngine = st.session_state.engine
|
||||
thermal = engine.get_thermal_state()
|
||||
electrical = engine.get_electrical_state()
|
||||
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric("Chamber Temp", f"{thermal.chamber_temperature:.2f} °C")
|
||||
with col2:
|
||||
st.metric("Case Temp", f"{thermal.case_temperature:.2f} °C")
|
||||
with col3:
|
||||
st.metric("Junction Temp", f"{thermal.junction_temperature:.2f} °C")
|
||||
with col4:
|
||||
st.metric("Output Voltage", f"{electrical.output_voltage:.4f} V")
|
||||
|
||||
col5, col6, col7, col8 = st.columns(4)
|
||||
|
||||
with col5:
|
||||
st.metric("Input Voltage", f"{electrical.input_voltage:.2f} V")
|
||||
with col6:
|
||||
st.metric("Load Current", f"{electrical.load_current * 1000:.1f} mA")
|
||||
with col7:
|
||||
st.metric("Power Diss.", f"{electrical.power_dissipation * 1000:.2f} mW")
|
||||
with col8:
|
||||
st.metric("Sim Time", f"{engine.simulation_time:.2f} s")
|
||||
|
||||
|
||||
def display_controls() -> None:
|
||||
"""Display simulation control panel in sidebar."""
|
||||
engine: PhysicsEngine = st.session_state.engine
|
||||
|
||||
st.sidebar.header("Simulation Controls")
|
||||
|
||||
# Start/Stop button
|
||||
if st.session_state.running:
|
||||
if st.sidebar.button("Stop Simulation", type="primary", use_container_width=True):
|
||||
st.session_state.running = False
|
||||
st.rerun()
|
||||
else:
|
||||
if st.sidebar.button(
|
||||
"Start Simulation", type="primary", use_container_width=True
|
||||
):
|
||||
st.session_state.running = True
|
||||
st.rerun()
|
||||
|
||||
# Reset button
|
||||
if st.sidebar.button("Reset", use_container_width=True):
|
||||
st.session_state.engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
st.session_state.history = SimulationHistory()
|
||||
st.session_state.running = False
|
||||
st.rerun()
|
||||
|
||||
st.sidebar.divider()
|
||||
|
||||
# Temperature setpoint
|
||||
st.sidebar.subheader("Thermal Chamber")
|
||||
temp_setpoint = st.sidebar.slider(
|
||||
"Temperature Setpoint (°C)",
|
||||
min_value=-40.0,
|
||||
max_value=125.0,
|
||||
value=25.0,
|
||||
step=5.0,
|
||||
key="temp_setpoint",
|
||||
)
|
||||
engine.set_chamber_setpoint(temp_setpoint)
|
||||
|
||||
st.sidebar.divider()
|
||||
|
||||
# Power supply controls
|
||||
st.sidebar.subheader("Power Supply")
|
||||
input_voltage = st.sidebar.slider(
|
||||
"Input Voltage (V)",
|
||||
min_value=0.0,
|
||||
max_value=12.0,
|
||||
value=5.0,
|
||||
step=0.1,
|
||||
key="input_voltage",
|
||||
)
|
||||
engine.set_input_voltage(input_voltage)
|
||||
|
||||
output_enabled = st.sidebar.toggle(
|
||||
"Output Enabled",
|
||||
value=engine.is_output_enabled,
|
||||
key="output_enabled",
|
||||
)
|
||||
engine.set_output_enabled(output_enabled)
|
||||
|
||||
st.sidebar.divider()
|
||||
|
||||
# Load controls
|
||||
st.sidebar.subheader("Electronic Load")
|
||||
load_current_ma = st.sidebar.slider(
|
||||
"Load Current (mA)",
|
||||
min_value=0.0,
|
||||
max_value=500.0,
|
||||
value=100.0,
|
||||
step=10.0,
|
||||
key="load_current",
|
||||
)
|
||||
engine.set_load_current(load_current_ma / 1000.0)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Main entry point for the Streamlit dashboard."""
|
||||
st.set_page_config(
|
||||
page_title="py-dvt-ate Virtual Lab Bench",
|
||||
page_icon="🔬",
|
||||
layout="wide",
|
||||
)
|
||||
|
||||
st.title("py-dvt-ate Virtual Lab Bench")
|
||||
st.markdown(
|
||||
"""
|
||||
Interactive physics simulation demonstrating coupled thermal-electrical
|
||||
behaviour of an LDO voltage regulator.
|
||||
"""
|
||||
)
|
||||
|
||||
init_session_state()
|
||||
|
||||
# Sidebar controls
|
||||
display_controls()
|
||||
|
||||
# Current state display
|
||||
st.subheader("Current State")
|
||||
display_current_state()
|
||||
|
||||
# Temperature chart
|
||||
st.subheader("Temperature History")
|
||||
display_thermal_chart()
|
||||
|
||||
# Self-heating demonstration
|
||||
st.subheader("Self-Heating Demonstration")
|
||||
display_self_heating_panel()
|
||||
|
||||
# Auto-refresh when running
|
||||
if st.session_state.running:
|
||||
step_simulation(steps=10)
|
||||
st.rerun()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1 +0,0 @@
|
||||
"""Command-line interface."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Configuration handling."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Streamlit dashboard."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Instrument SCPI drivers."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Test execution framework."""
|
||||
5
src/py_dvt_ate/framework/__init__.py
Normal file
5
src/py_dvt_ate/framework/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Test execution framework.
|
||||
|
||||
Provides test sequencing, measurement logging, limit checking,
|
||||
and runtime context management for DVT characterisation tests.
|
||||
"""
|
||||
@@ -1 +0,0 @@
|
||||
"""Hardware Abstraction Layer."""
|
||||
@@ -1 +0,0 @@
|
||||
"""HAL implementations."""
|
||||
@@ -1 +1,9 @@
|
||||
"""Virtual instrument implementations."""
|
||||
"""Instrument control package.
|
||||
|
||||
This package provides everything needed to communicate with lab instruments:
|
||||
- Protocol interfaces (IThermalChamber, IPowerSupply, IMultimeter)
|
||||
- SCPI command parsing
|
||||
- Transport layer (TCP, VISA)
|
||||
- Instrument drivers
|
||||
- Factory for creating configured instrument sets
|
||||
"""
|
||||
|
||||
5
src/py_dvt_ate/instruments/drivers/__init__.py
Normal file
5
src/py_dvt_ate/instruments/drivers/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""SCPI driver implementations for lab instruments.
|
||||
|
||||
Each driver translates high-level operations into SCPI commands
|
||||
and handles responses from instruments.
|
||||
"""
|
||||
6
src/py_dvt_ate/instruments/transport/__init__.py
Normal file
6
src/py_dvt_ate/instruments/transport/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Transport layer for instrument communication.
|
||||
|
||||
Provides connection abstractions for different backends:
|
||||
- TCP sockets (for simulation server)
|
||||
- PyVISA (for real instruments)
|
||||
"""
|
||||
@@ -1 +0,0 @@
|
||||
"""Physics simulation engine."""
|
||||
@@ -1 +0,0 @@
|
||||
"""Device Under Test models."""
|
||||
5
src/py_dvt_ate/reporting/__init__.py
Normal file
5
src/py_dvt_ate/reporting/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Report generation.
|
||||
|
||||
Generates test reports from stored data in various formats
|
||||
including PDF and HTML.
|
||||
"""
|
||||
@@ -1 +0,0 @@
|
||||
"""Simulation server."""
|
||||
5
src/py_dvt_ate/simulation/__init__.py
Normal file
5
src/py_dvt_ate/simulation/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Physics simulation package.
|
||||
|
||||
Provides virtual instruments backed by a coupled thermal-electrical
|
||||
physics engine. Used for development and testing without real hardware.
|
||||
"""
|
||||
5
src/py_dvt_ate/simulation/physics/__init__.py
Normal file
5
src/py_dvt_ate/simulation/physics/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Physics engine for thermal-electrical simulation.
|
||||
|
||||
Implements coupled thermal and electrical domain models with
|
||||
realistic time constants and temperature-dependent behaviour.
|
||||
"""
|
||||
222
src/py_dvt_ate/simulation/physics/engine.py
Normal file
222
src/py_dvt_ate/simulation/physics/engine.py
Normal file
@@ -0,0 +1,222 @@
|
||||
"""Physics engine for coupled thermal-electrical simulation.
|
||||
|
||||
The physics engine maintains the simulation state and advances it
|
||||
in discrete time steps, modelling the thermal and electrical coupling
|
||||
between the DUT and its environment.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from py_dvt_ate.simulation.physics.state import ElectricalState, ThermalState
|
||||
from py_dvt_ate.simulation.physics.thermal import (
|
||||
calculate_junction_temperature,
|
||||
update_case_temperature,
|
||||
update_temperature,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from py_dvt_ate.simulation.physics.models.base import DUTModel
|
||||
|
||||
|
||||
class PhysicsEngine:
|
||||
"""Coupled thermal-electrical physics simulation.
|
||||
|
||||
Runs at a fixed timestep, updating thermal and electrical state
|
||||
based on the DUT model and environmental conditions.
|
||||
|
||||
The simulation models:
|
||||
- Chamber temperature approaching setpoint (first-order response)
|
||||
- Case temperature driven by chamber and self-heating
|
||||
- Junction temperature from case temperature and thermal resistance
|
||||
- Electrical behaviour from the DUT model (temperature-dependent)
|
||||
|
||||
Attributes:
|
||||
dt: Simulation timestep in seconds.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
update_rate_hz: float = 100.0,
|
||||
dut_model: DUTModel | None = None,
|
||||
) -> None:
|
||||
"""Initialise the physics engine.
|
||||
|
||||
Args:
|
||||
update_rate_hz: Simulation update rate in Hz. Defaults to 100.
|
||||
dut_model: DUT model to use for electrical calculations.
|
||||
If None, a default LDO model will be used.
|
||||
"""
|
||||
self.dt = 1.0 / update_rate_hz
|
||||
|
||||
# Lazily import to avoid circular dependencies
|
||||
if dut_model is None:
|
||||
from py_dvt_ate.simulation.physics.models.ldo import LDOModel
|
||||
|
||||
self._dut: DUTModel = LDOModel()
|
||||
else:
|
||||
self._dut = dut_model
|
||||
|
||||
# Thermal parameters
|
||||
self._tau_chamber = 30.0 # seconds
|
||||
self._tau_case = 5.0 # seconds
|
||||
self._theta_jc = 15.0 # degC/W
|
||||
self._theta_ca = 5.0 # degC/W
|
||||
|
||||
# State variables
|
||||
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.
|
||||
|
||||
Updates thermal and electrical state based on current conditions.
|
||||
The thermal-electrical coupling works as follows:
|
||||
1. Calculate current power dissipation from DUT model
|
||||
2. Update chamber temperature towards setpoint
|
||||
3. Update case temperature including self-heating
|
||||
4. Advance simulation time
|
||||
"""
|
||||
# Calculate power dissipation (uses current junction temperature estimate)
|
||||
p_diss = self._calculate_power_dissipation()
|
||||
|
||||
# Update chamber temperature (first-order response to setpoint)
|
||||
self._t_chamber = update_temperature(
|
||||
current_temperature=self._t_chamber,
|
||||
target_temperature=self._t_setpoint,
|
||||
time_constant=self._tau_chamber,
|
||||
dt=self.dt,
|
||||
)
|
||||
|
||||
# Update case temperature (driven by chamber + self-heating)
|
||||
self._t_case = update_case_temperature(
|
||||
case_temperature=self._t_case,
|
||||
ambient_temperature=self._t_chamber,
|
||||
power_dissipation=p_diss,
|
||||
time_constant=self._tau_case,
|
||||
theta_ca=self._theta_ca,
|
||||
dt=self.dt,
|
||||
)
|
||||
|
||||
# Advance simulation time
|
||||
self._sim_time += self.dt
|
||||
|
||||
def get_thermal_state(self) -> ThermalState:
|
||||
"""Get current thermal state snapshot.
|
||||
|
||||
Returns:
|
||||
Immutable ThermalState with current temperatures.
|
||||
"""
|
||||
p_diss = self._calculate_power_dissipation()
|
||||
t_junction = calculate_junction_temperature(
|
||||
case_temperature=self._t_case,
|
||||
power_dissipation=p_diss,
|
||||
theta_jc=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 snapshot.
|
||||
|
||||
Returns:
|
||||
Immutable ElectricalState with current electrical values.
|
||||
"""
|
||||
p_diss = self._calculate_power_dissipation()
|
||||
t_junction = calculate_junction_temperature(
|
||||
case_temperature=self._t_case,
|
||||
power_dissipation=p_diss,
|
||||
theta_jc=self._theta_jc,
|
||||
)
|
||||
|
||||
if self._output_enabled:
|
||||
v_out = self._dut.calculate_output_voltage(t_junction)
|
||||
i_q = self._dut.calculate_quiescent_current(t_junction)
|
||||
i_load = self._i_load
|
||||
else:
|
||||
v_out = 0.0
|
||||
i_q = 0.0
|
||||
i_load = 0.0
|
||||
|
||||
return ElectricalState(
|
||||
input_voltage=self._v_in,
|
||||
output_voltage=v_out,
|
||||
load_current=i_load,
|
||||
quiescent_current=i_q,
|
||||
power_dissipation=p_diss,
|
||||
)
|
||||
|
||||
def _calculate_power_dissipation(self) -> float:
|
||||
"""Calculate current power dissipation.
|
||||
|
||||
Uses the current case temperature as an approximation for junction
|
||||
temperature in the power calculation. The true junction temperature
|
||||
depends on power dissipation, creating a feedback loop that is
|
||||
resolved iteratively through the simulation steps.
|
||||
|
||||
Returns:
|
||||
Power dissipation in watts.
|
||||
"""
|
||||
if not self._output_enabled:
|
||||
return 0.0
|
||||
|
||||
# Use case temperature as junction estimate for power calculation
|
||||
# This avoids circular dependency in the calculation
|
||||
return self._dut.calculate_power_dissipation(
|
||||
input_voltage=self._v_in,
|
||||
load_current=self._i_load,
|
||||
junction_temperature=self._t_case,
|
||||
)
|
||||
|
||||
def set_chamber_setpoint(self, temperature: float) -> None:
|
||||
"""Set chamber target temperature.
|
||||
|
||||
Args:
|
||||
temperature: Target temperature in degrees Celsius.
|
||||
"""
|
||||
self._t_setpoint = temperature
|
||||
|
||||
def set_input_voltage(self, voltage: float) -> None:
|
||||
"""Set DUT input voltage.
|
||||
|
||||
Args:
|
||||
voltage: Input voltage in volts.
|
||||
"""
|
||||
self._v_in = voltage
|
||||
|
||||
def set_load_current(self, current: float) -> None:
|
||||
"""Set DUT load current.
|
||||
|
||||
Args:
|
||||
current: Load current in amps.
|
||||
"""
|
||||
self._i_load = current
|
||||
|
||||
def set_output_enabled(self, enabled: bool) -> None:
|
||||
"""Enable or disable DUT power.
|
||||
|
||||
Args:
|
||||
enabled: True to enable output, False to disable.
|
||||
"""
|
||||
self._output_enabled = enabled
|
||||
|
||||
@property
|
||||
def simulation_time(self) -> float:
|
||||
"""Get current simulation time in seconds."""
|
||||
return self._sim_time
|
||||
|
||||
@property
|
||||
def is_output_enabled(self) -> bool:
|
||||
"""Check if DUT output is enabled."""
|
||||
return self._output_enabled
|
||||
10
src/py_dvt_ate/simulation/physics/models/__init__.py
Normal file
10
src/py_dvt_ate/simulation/physics/models/__init__.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""Device Under Test (DUT) models.
|
||||
|
||||
Provides thermal and electrical models for various device types
|
||||
including LDO regulators, op-amps, and other components.
|
||||
"""
|
||||
|
||||
from py_dvt_ate.simulation.physics.models.base import DUTModel
|
||||
from py_dvt_ate.simulation.physics.models.ldo import LDOModel, LDOParameters
|
||||
|
||||
__all__ = ["DUTModel", "LDOModel", "LDOParameters"]
|
||||
61
src/py_dvt_ate/simulation/physics/models/base.py
Normal file
61
src/py_dvt_ate/simulation/physics/models/base.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""Base protocol for Device Under Test (DUT) models.
|
||||
|
||||
Defines the interface that all DUT models must implement to integrate
|
||||
with the physics engine.
|
||||
"""
|
||||
|
||||
from typing import Protocol, runtime_checkable
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class DUTModel(Protocol):
|
||||
"""Protocol for DUT electrical/thermal models.
|
||||
|
||||
DUT models encapsulate the temperature-dependent electrical behaviour
|
||||
of a device, enabling realistic simulation of thermal-electrical coupling.
|
||||
|
||||
All temperature parameters are in degrees Celsius.
|
||||
All voltage parameters are in volts.
|
||||
All current parameters are in amps.
|
||||
All power parameters are in watts.
|
||||
"""
|
||||
|
||||
def calculate_output_voltage(self, junction_temperature: float) -> float:
|
||||
"""Calculate the output voltage at the given junction temperature.
|
||||
|
||||
Args:
|
||||
junction_temperature: DUT junction temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
Output voltage in volts.
|
||||
"""
|
||||
...
|
||||
|
||||
def calculate_quiescent_current(self, junction_temperature: float) -> float:
|
||||
"""Calculate the quiescent current at the given junction temperature.
|
||||
|
||||
Args:
|
||||
junction_temperature: DUT junction temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
Quiescent current in amps.
|
||||
"""
|
||||
...
|
||||
|
||||
def calculate_power_dissipation(
|
||||
self,
|
||||
input_voltage: float,
|
||||
load_current: float,
|
||||
junction_temperature: float,
|
||||
) -> float:
|
||||
"""Calculate the power dissipation for given operating conditions.
|
||||
|
||||
Args:
|
||||
input_voltage: Input voltage in volts.
|
||||
load_current: Load current in amps.
|
||||
junction_temperature: DUT junction temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
Power dissipation in watts.
|
||||
"""
|
||||
...
|
||||
219
src/py_dvt_ate/simulation/physics/models/ldo.py
Normal file
219
src/py_dvt_ate/simulation/physics/models/ldo.py
Normal file
@@ -0,0 +1,219 @@
|
||||
"""LDO (Low Dropout Regulator) DUT model.
|
||||
|
||||
Implements temperature-dependent electrical behaviour for an LDO voltage
|
||||
regulator, including output voltage tempco, quiescent current variation,
|
||||
and power dissipation calculations.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class LDOParameters:
|
||||
"""Configuration parameters for an LDO model.
|
||||
|
||||
All temperature coefficients are referenced to 25°C.
|
||||
|
||||
Attributes:
|
||||
nominal_output_voltage: Nominal output voltage at 25°C in volts.
|
||||
tempco_ppm_per_c: Output voltage temperature coefficient in ppm/°C.
|
||||
quiescent_current_a: Quiescent current at 25°C in amps.
|
||||
quiescent_current_tempco: Quiescent current temperature coefficient (1/°C).
|
||||
dropout_voltage: Dropout voltage at 25°C in volts.
|
||||
max_output_current: Maximum output current in amps.
|
||||
"""
|
||||
|
||||
nominal_output_voltage: float = 3.3
|
||||
tempco_ppm_per_c: float = 50.0
|
||||
quiescent_current_a: float = 50e-6 # 50 µA
|
||||
quiescent_current_tempco: float = 0.003 # 0.3%/°C
|
||||
dropout_voltage: float = 0.3
|
||||
max_output_current: float = 0.5
|
||||
|
||||
|
||||
# Reference temperature for all calculations
|
||||
REFERENCE_TEMPERATURE_C = 25.0
|
||||
|
||||
|
||||
class LDOModel:
|
||||
"""Temperature-dependent LDO voltage regulator model.
|
||||
|
||||
Models the electrical behaviour of a linear voltage regulator with:
|
||||
- Output voltage that varies with temperature (tempco in ppm/°C)
|
||||
- Quiescent current that varies with temperature
|
||||
- Dropout voltage that increases with temperature
|
||||
- Power dissipation from (Vin - Vout) × Iload + Vin × Iq
|
||||
|
||||
This class implements the DUTModel protocol.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
params: LDOParameters | None = None,
|
||||
input_voltage: float = 5.0,
|
||||
load_current: float = 0.0,
|
||||
) -> None:
|
||||
"""Initialise the LDO model.
|
||||
|
||||
Args:
|
||||
params: LDO parameters. Uses defaults if None.
|
||||
input_voltage: Initial input voltage in volts.
|
||||
load_current: Initial load current in amps.
|
||||
"""
|
||||
self._params = params or LDOParameters()
|
||||
self._input_voltage = input_voltage
|
||||
self._load_current = load_current
|
||||
|
||||
@property
|
||||
def params(self) -> LDOParameters:
|
||||
"""Get the LDO parameters."""
|
||||
return self._params
|
||||
|
||||
@property
|
||||
def input_voltage(self) -> float:
|
||||
"""Get the current input voltage."""
|
||||
return self._input_voltage
|
||||
|
||||
@input_voltage.setter
|
||||
def input_voltage(self, value: float) -> None:
|
||||
"""Set the input voltage."""
|
||||
self._input_voltage = value
|
||||
|
||||
@property
|
||||
def load_current(self) -> float:
|
||||
"""Get the current load current."""
|
||||
return self._load_current
|
||||
|
||||
@load_current.setter
|
||||
def load_current(self, value: float) -> None:
|
||||
"""Set the load current."""
|
||||
self._load_current = value
|
||||
|
||||
def calculate_output_voltage(self, junction_temperature: float) -> float:
|
||||
"""Calculate the output voltage at the given junction temperature.
|
||||
|
||||
Implements: V_out(T) = V_nom × (1 + TC_vout × (T - 25) × 1e-6)
|
||||
|
||||
The output voltage is clamped to not exceed (Vin - Vdropout) when
|
||||
the regulator is in dropout.
|
||||
|
||||
Args:
|
||||
junction_temperature: DUT junction temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
Output voltage in volts.
|
||||
"""
|
||||
delta_t = junction_temperature - REFERENCE_TEMPERATURE_C
|
||||
tempco_factor = 1.0 + self._params.tempco_ppm_per_c * delta_t * 1e-6
|
||||
|
||||
ideal_vout = self._params.nominal_output_voltage * tempco_factor
|
||||
|
||||
# Calculate dropout voltage at temperature
|
||||
v_dropout = self._calculate_dropout_voltage(junction_temperature)
|
||||
|
||||
# Clamp output to not exceed (Vin - Vdropout)
|
||||
max_vout = max(0.0, self._input_voltage - v_dropout)
|
||||
|
||||
return min(ideal_vout, max_vout)
|
||||
|
||||
def calculate_quiescent_current(self, junction_temperature: float) -> float:
|
||||
"""Calculate the quiescent current at the given junction temperature.
|
||||
|
||||
Implements: I_q(T) = I_q_25 × (1 + TC_iq × (T - 25))
|
||||
|
||||
Args:
|
||||
junction_temperature: DUT junction temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
Quiescent current in amps.
|
||||
"""
|
||||
delta_t = junction_temperature - REFERENCE_TEMPERATURE_C
|
||||
tempco_factor = 1.0 + self._params.quiescent_current_tempco * delta_t
|
||||
|
||||
return self._params.quiescent_current_a * tempco_factor
|
||||
|
||||
def calculate_power_dissipation(
|
||||
self,
|
||||
input_voltage: float,
|
||||
load_current: float,
|
||||
junction_temperature: float,
|
||||
) -> float:
|
||||
"""Calculate the power dissipation for given operating conditions.
|
||||
|
||||
Implements: P_diss = (V_in - V_out) × I_load + V_in × I_q
|
||||
|
||||
The power dissipation comes from:
|
||||
- Voltage drop across the pass element times load current
|
||||
- Quiescent current times input voltage
|
||||
|
||||
Args:
|
||||
input_voltage: Input voltage in volts.
|
||||
load_current: Load current in amps.
|
||||
junction_temperature: DUT junction temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
Power dissipation in watts.
|
||||
"""
|
||||
# Temporarily set input voltage for output voltage calculation
|
||||
original_vin = self._input_voltage
|
||||
self._input_voltage = input_voltage
|
||||
|
||||
try:
|
||||
v_out = self.calculate_output_voltage(junction_temperature)
|
||||
i_q = self.calculate_quiescent_current(junction_temperature)
|
||||
|
||||
# Power in pass element
|
||||
p_pass = (input_voltage - v_out) * load_current
|
||||
|
||||
# Power from quiescent current
|
||||
p_quiescent = input_voltage * i_q
|
||||
|
||||
return p_pass + p_quiescent
|
||||
finally:
|
||||
self._input_voltage = original_vin
|
||||
|
||||
def _calculate_dropout_voltage(self, junction_temperature: float) -> float:
|
||||
"""Calculate the dropout voltage at the given temperature.
|
||||
|
||||
Implements: V_do(T) = V_do_25 × (T_K / 300)^1.5
|
||||
|
||||
where T_K is the temperature in Kelvin.
|
||||
|
||||
Dropout voltage increases with temperature due to increased
|
||||
resistance of the pass element.
|
||||
|
||||
Args:
|
||||
junction_temperature: Junction temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
Dropout voltage in volts.
|
||||
"""
|
||||
# Convert to Kelvin
|
||||
t_kelvin = junction_temperature + 273.15
|
||||
|
||||
# Temperature ratio (reference is approximately 300K ≈ 27°C)
|
||||
temp_ratio = t_kelvin / 300.0
|
||||
|
||||
return self._params.dropout_voltage * (temp_ratio**1.5)
|
||||
|
||||
def is_in_dropout(self, junction_temperature: float) -> bool:
|
||||
"""Check if the LDO is in dropout at current operating point.
|
||||
|
||||
The LDO is in dropout when the input voltage minus dropout voltage
|
||||
is less than the nominal output voltage.
|
||||
|
||||
Args:
|
||||
junction_temperature: Junction temperature in degrees Celsius.
|
||||
|
||||
Returns:
|
||||
True if in dropout, False otherwise.
|
||||
"""
|
||||
v_dropout = self._calculate_dropout_voltage(junction_temperature)
|
||||
headroom = self._input_voltage - v_dropout
|
||||
|
||||
# Get the ideal (temperature-adjusted) output voltage
|
||||
delta_t = junction_temperature - REFERENCE_TEMPERATURE_C
|
||||
tempco_factor = 1.0 + self._params.tempco_ppm_per_c * delta_t * 1e-6
|
||||
ideal_vout = self._params.nominal_output_voltage * tempco_factor
|
||||
|
||||
return headroom < ideal_vout
|
||||
48
src/py_dvt_ate/simulation/physics/state.py
Normal file
48
src/py_dvt_ate/simulation/physics/state.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Physics state dataclasses for thermal-electrical simulation.
|
||||
|
||||
These immutable state snapshots represent the simulation state at a point in time.
|
||||
"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ThermalState:
|
||||
"""Immutable thermal state snapshot.
|
||||
|
||||
Represents the thermal conditions of the DUT and its environment
|
||||
at a specific point in simulation time.
|
||||
|
||||
Attributes:
|
||||
chamber_temperature: Chamber air temperature in degrees Celsius.
|
||||
case_temperature: DUT case/package temperature in degrees Celsius.
|
||||
junction_temperature: DUT junction/die temperature in degrees Celsius.
|
||||
timestamp: Simulation time in seconds since start.
|
||||
"""
|
||||
|
||||
chamber_temperature: float
|
||||
case_temperature: float
|
||||
junction_temperature: float
|
||||
timestamp: float
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ElectricalState:
|
||||
"""Immutable electrical state snapshot.
|
||||
|
||||
Represents the electrical conditions of the DUT at a specific
|
||||
point in simulation time.
|
||||
|
||||
Attributes:
|
||||
input_voltage: DUT input voltage in volts.
|
||||
output_voltage: DUT output voltage in volts.
|
||||
load_current: Load current drawn from DUT in amps.
|
||||
quiescent_current: DUT quiescent/bias current in amps.
|
||||
power_dissipation: Total power dissipated by DUT in watts.
|
||||
"""
|
||||
|
||||
input_voltage: float
|
||||
output_voltage: float
|
||||
load_current: float
|
||||
quiescent_current: float
|
||||
power_dissipation: float
|
||||
193
src/py_dvt_ate/simulation/physics/thermal.py
Normal file
193
src/py_dvt_ate/simulation/physics/thermal.py
Normal file
@@ -0,0 +1,193 @@
|
||||
"""Thermal calculation functions for physics simulation.
|
||||
|
||||
Pure functions implementing first-order thermal response calculations
|
||||
for the coupled thermal-electrical simulation.
|
||||
|
||||
All temperatures are in degrees Celsius.
|
||||
All time values are in seconds.
|
||||
All power values are in watts.
|
||||
All thermal resistances are in degrees Celsius per watt (°C/W).
|
||||
"""
|
||||
|
||||
|
||||
def calculate_temperature_derivative(
|
||||
current_temperature: float,
|
||||
target_temperature: float,
|
||||
time_constant: float,
|
||||
) -> float:
|
||||
"""Calculate the rate of temperature change for first-order response.
|
||||
|
||||
Implements: dT/dt = (T_target - T_current) / τ
|
||||
|
||||
Args:
|
||||
current_temperature: Current temperature in degrees Celsius.
|
||||
target_temperature: Target temperature in degrees Celsius.
|
||||
time_constant: Thermal time constant in seconds.
|
||||
|
||||
Returns:
|
||||
Rate of temperature change in degrees Celsius per second.
|
||||
|
||||
Raises:
|
||||
ValueError: If time_constant is not positive.
|
||||
"""
|
||||
if time_constant <= 0:
|
||||
msg = f"Time constant must be positive, got {time_constant}"
|
||||
raise ValueError(msg)
|
||||
|
||||
return (target_temperature - current_temperature) / time_constant
|
||||
|
||||
|
||||
def update_temperature(
|
||||
current_temperature: float,
|
||||
target_temperature: float,
|
||||
time_constant: float,
|
||||
dt: float,
|
||||
) -> float:
|
||||
"""Calculate new temperature after a time step using Euler integration.
|
||||
|
||||
Args:
|
||||
current_temperature: Current temperature in degrees Celsius.
|
||||
target_temperature: Target temperature in degrees Celsius.
|
||||
time_constant: Thermal time constant in seconds.
|
||||
dt: Time step in seconds.
|
||||
|
||||
Returns:
|
||||
New temperature in degrees Celsius after the time step.
|
||||
|
||||
Raises:
|
||||
ValueError: If time_constant or dt is not positive.
|
||||
"""
|
||||
if dt <= 0:
|
||||
msg = f"Time step must be positive, got {dt}"
|
||||
raise ValueError(msg)
|
||||
|
||||
derivative = calculate_temperature_derivative(
|
||||
current_temperature, target_temperature, time_constant
|
||||
)
|
||||
return current_temperature + derivative * dt
|
||||
|
||||
|
||||
def calculate_case_temperature_derivative(
|
||||
case_temperature: float,
|
||||
ambient_temperature: float,
|
||||
power_dissipation: float,
|
||||
time_constant: float,
|
||||
theta_ca: float,
|
||||
) -> float:
|
||||
"""Calculate rate of case temperature change including self-heating.
|
||||
|
||||
Implements: dT_case/dt = (T_ambient - T_case + P_diss × θ_ca) / τ_case
|
||||
|
||||
The case temperature is driven by:
|
||||
- Convection/conduction to ambient (chamber) temperature
|
||||
- Self-heating from power dissipation through case-to-ambient thermal resistance
|
||||
|
||||
Args:
|
||||
case_temperature: Current case temperature in degrees Celsius.
|
||||
ambient_temperature: Ambient (chamber) temperature in degrees Celsius.
|
||||
power_dissipation: Power dissipated by the device in watts.
|
||||
time_constant: Case thermal time constant in seconds.
|
||||
theta_ca: Thermal resistance from case to ambient in °C/W.
|
||||
|
||||
Returns:
|
||||
Rate of case temperature change in degrees Celsius per second.
|
||||
|
||||
Raises:
|
||||
ValueError: If time_constant is not positive.
|
||||
"""
|
||||
if time_constant <= 0:
|
||||
msg = f"Time constant must be positive, got {time_constant}"
|
||||
raise ValueError(msg)
|
||||
|
||||
# The effective target includes self-heating contribution
|
||||
thermal_drive = ambient_temperature - case_temperature + power_dissipation * theta_ca
|
||||
return thermal_drive / time_constant
|
||||
|
||||
|
||||
def update_case_temperature(
|
||||
case_temperature: float,
|
||||
ambient_temperature: float,
|
||||
power_dissipation: float,
|
||||
time_constant: float,
|
||||
theta_ca: float,
|
||||
dt: float,
|
||||
) -> float:
|
||||
"""Calculate new case temperature after a time step.
|
||||
|
||||
Args:
|
||||
case_temperature: Current case temperature in degrees Celsius.
|
||||
ambient_temperature: Ambient (chamber) temperature in degrees Celsius.
|
||||
power_dissipation: Power dissipated by the device in watts.
|
||||
time_constant: Case thermal time constant in seconds.
|
||||
theta_ca: Thermal resistance from case to ambient in °C/W.
|
||||
dt: Time step in seconds.
|
||||
|
||||
Returns:
|
||||
New case temperature in degrees Celsius after the time step.
|
||||
|
||||
Raises:
|
||||
ValueError: If time_constant or dt is not positive.
|
||||
"""
|
||||
if dt <= 0:
|
||||
msg = f"Time step must be positive, got {dt}"
|
||||
raise ValueError(msg)
|
||||
|
||||
derivative = calculate_case_temperature_derivative(
|
||||
case_temperature,
|
||||
ambient_temperature,
|
||||
power_dissipation,
|
||||
time_constant,
|
||||
theta_ca,
|
||||
)
|
||||
return case_temperature + derivative * dt
|
||||
|
||||
|
||||
def calculate_junction_temperature(
|
||||
case_temperature: float,
|
||||
power_dissipation: float,
|
||||
theta_jc: float,
|
||||
) -> float:
|
||||
"""Calculate junction temperature from case temperature and power.
|
||||
|
||||
The junction temperature is assumed to respond instantaneously to
|
||||
changes in case temperature and power dissipation (no thermal mass
|
||||
at the die level for this simplified model).
|
||||
|
||||
Implements: T_junction = T_case + P_diss × θ_jc
|
||||
|
||||
Args:
|
||||
case_temperature: Case temperature in degrees Celsius.
|
||||
power_dissipation: Power dissipated by the device in watts.
|
||||
theta_jc: Thermal resistance from junction to case in °C/W.
|
||||
|
||||
Returns:
|
||||
Junction temperature in degrees Celsius.
|
||||
"""
|
||||
return case_temperature + power_dissipation * theta_jc
|
||||
|
||||
|
||||
def calculate_steady_state_junction_temperature(
|
||||
ambient_temperature: float,
|
||||
power_dissipation: float,
|
||||
theta_jc: float,
|
||||
theta_ca: float,
|
||||
) -> float:
|
||||
"""Calculate the steady-state junction temperature.
|
||||
|
||||
At steady state, the case temperature reaches equilibrium where
|
||||
the heat flow through θ_ca equals the power dissipation.
|
||||
|
||||
T_case_ss = T_ambient + P_diss × θ_ca
|
||||
T_junction_ss = T_case_ss + P_diss × θ_jc
|
||||
= T_ambient + P_diss × (θ_jc + θ_ca)
|
||||
|
||||
Args:
|
||||
ambient_temperature: Ambient (chamber) temperature in degrees Celsius.
|
||||
power_dissipation: Power dissipated by the device in watts.
|
||||
theta_jc: Thermal resistance from junction to case in °C/W.
|
||||
theta_ca: Thermal resistance from case to ambient in °C/W.
|
||||
|
||||
Returns:
|
||||
Steady-state junction temperature in degrees Celsius.
|
||||
"""
|
||||
return ambient_temperature + power_dissipation * (theta_jc + theta_ca)
|
||||
5
src/py_dvt_ate/simulation/virtual/__init__.py
Normal file
5
src/py_dvt_ate/simulation/virtual/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Virtual instrument implementations.
|
||||
|
||||
SCPI-compliant virtual instruments that respond like real hardware
|
||||
but are backed by the physics simulation engine.
|
||||
"""
|
||||
@@ -1 +1,6 @@
|
||||
"""DVT test implementations."""
|
||||
"""DVT test implementations.
|
||||
|
||||
Contains characterisation test suites organised by category:
|
||||
- thermal/ - Temperature-related tests (TempCo, etc.)
|
||||
- electrical/ - Electrical tests (load regulation, etc.)
|
||||
"""
|
||||
|
||||
5
src/py_dvt_ate/tests/electrical/__init__.py
Normal file
5
src/py_dvt_ate/tests/electrical/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Electrical characterisation tests.
|
||||
|
||||
Tests for electrical performance including load regulation,
|
||||
line regulation, and output accuracy.
|
||||
"""
|
||||
5
src/py_dvt_ate/tests/thermal/__init__.py
Normal file
5
src/py_dvt_ate/tests/thermal/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
"""Thermal characterisation tests.
|
||||
|
||||
Tests related to temperature behaviour including temperature
|
||||
coefficient (TempCo) measurements and thermal stability.
|
||||
"""
|
||||
@@ -1 +0,0 @@
|
||||
"""Communication layer."""
|
||||
300
tests/unit/test_physics_engine.py
Normal file
300
tests/unit/test_physics_engine.py
Normal file
@@ -0,0 +1,300 @@
|
||||
"""Integration tests for the physics engine with thermal-electrical coupling.
|
||||
|
||||
Tests the complete physics simulation including:
|
||||
- Thermal settling behaviour (chamber, case, junction)
|
||||
- Self-heating effects from power dissipation
|
||||
- Temperature-dependent electrical behaviour
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from py_dvt_ate.simulation.physics.engine import PhysicsEngine
|
||||
from py_dvt_ate.simulation.physics.models.ldo import LDOModel, LDOParameters
|
||||
|
||||
|
||||
class TestThermalSettling:
|
||||
"""Tests for thermal settling behaviour."""
|
||||
|
||||
def test_chamber_approaches_setpoint(self) -> None:
|
||||
"""Test chamber temperature approaches setpoint over time."""
|
||||
engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
# Set a new setpoint
|
||||
engine.set_chamber_setpoint(85.0)
|
||||
|
||||
# Initial state
|
||||
initial_state = engine.get_thermal_state()
|
||||
assert initial_state.chamber_temperature == pytest.approx(25.0)
|
||||
|
||||
# Simulate for 1 second (100 steps at 100Hz)
|
||||
for _ in range(100):
|
||||
engine.step()
|
||||
|
||||
state_1s = engine.get_thermal_state()
|
||||
|
||||
# Chamber should have moved towards 85°C but not reached it yet
|
||||
# With tau=30s, after 1s: T = 25 + (85-25)*(1 - e^(-1/30)) ≈ 27.0°C
|
||||
assert state_1s.chamber_temperature > 25.0
|
||||
assert state_1s.chamber_temperature < 85.0
|
||||
|
||||
# Simulate for another 89 seconds (total 90s = 3*tau)
|
||||
for _ in range(8900):
|
||||
engine.step()
|
||||
|
||||
state_90s = engine.get_thermal_state()
|
||||
|
||||
# After 3 time constants, should be ~95% of the way there
|
||||
# T = 25 + (85-25)*(1 - e^(-3)) ≈ 82.0°C
|
||||
assert state_90s.chamber_temperature > 80.0
|
||||
assert state_90s.chamber_temperature < 85.0
|
||||
|
||||
def test_case_follows_chamber(self) -> None:
|
||||
"""Test case temperature follows chamber temperature."""
|
||||
engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
# Set chamber to high temperature (no power, so no self-heating)
|
||||
engine.set_chamber_setpoint(85.0)
|
||||
|
||||
# Simulate for 200 seconds (well past both time constants)
|
||||
for _ in range(20000):
|
||||
engine.step()
|
||||
|
||||
state = engine.get_thermal_state()
|
||||
|
||||
# With no power, case should approach chamber temperature
|
||||
assert state.case_temperature == pytest.approx(
|
||||
state.chamber_temperature, abs=0.5
|
||||
)
|
||||
|
||||
def test_junction_equals_case_with_no_power(self) -> None:
|
||||
"""Test junction equals case temperature when no power dissipated."""
|
||||
engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
# No power applied (output disabled by default)
|
||||
state = engine.get_thermal_state()
|
||||
|
||||
# Junction should equal case (no θ_jc rise with zero power)
|
||||
assert state.junction_temperature == pytest.approx(state.case_temperature)
|
||||
|
||||
|
||||
class TestSelfHeating:
|
||||
"""Tests for self-heating effects from power dissipation."""
|
||||
|
||||
def test_junction_higher_than_case_with_power(self) -> None:
|
||||
"""Test junction temperature rises above case when power is dissipated."""
|
||||
engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
# Apply power: 5V input, 100mA load
|
||||
engine.set_input_voltage(5.0)
|
||||
engine.set_load_current(0.1)
|
||||
engine.set_output_enabled(True)
|
||||
|
||||
# Let it settle
|
||||
for _ in range(1000):
|
||||
engine.step()
|
||||
|
||||
thermal_state = engine.get_thermal_state()
|
||||
electrical_state = engine.get_electrical_state()
|
||||
|
||||
# Power dissipation = (Vin - Vout) * Iload + Vin * Iq
|
||||
# With Vout ≈ 3.3V, P ≈ (5-3.3)*0.1 ≈ 0.17W
|
||||
assert electrical_state.power_dissipation > 0
|
||||
|
||||
# Junction should be higher than case by P * θ_jc
|
||||
# θ_jc = 15°C/W, so with 0.17W, ΔT ≈ 2.5°C
|
||||
assert (
|
||||
thermal_state.junction_temperature > thermal_state.case_temperature
|
||||
)
|
||||
|
||||
def test_self_heating_raises_case_temperature(self) -> None:
|
||||
"""Test self-heating raises case temperature above ambient."""
|
||||
engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
# Apply significant power: 5V input, 300mA load
|
||||
engine.set_input_voltage(5.0)
|
||||
engine.set_load_current(0.3)
|
||||
engine.set_output_enabled(True)
|
||||
|
||||
# Let thermal state settle (many time constants)
|
||||
for _ in range(50000): # 500 seconds at 100Hz
|
||||
engine.step()
|
||||
|
||||
thermal_state = engine.get_thermal_state()
|
||||
|
||||
# Power ≈ (5-3.3)*0.3 ≈ 0.51W
|
||||
# Steady-state case rise = P * θ_ca = 0.51 * 5 ≈ 2.5°C
|
||||
# Case should be above chamber temperature
|
||||
assert (
|
||||
thermal_state.case_temperature > thermal_state.chamber_temperature
|
||||
)
|
||||
|
||||
def test_self_heating_increases_with_load(self) -> None:
|
||||
"""Test that self-heating increases with higher load current."""
|
||||
engine1 = PhysicsEngine(update_rate_hz=100.0)
|
||||
engine2 = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
# Both at 5V, but different loads
|
||||
for engine, load in [(engine1, 0.1), (engine2, 0.3)]:
|
||||
engine.set_input_voltage(5.0)
|
||||
engine.set_load_current(load)
|
||||
engine.set_output_enabled(True)
|
||||
|
||||
# Let settle
|
||||
for _ in range(50000):
|
||||
engine.step()
|
||||
|
||||
state1 = engine1.get_thermal_state()
|
||||
state2 = engine2.get_thermal_state()
|
||||
|
||||
# Higher load should give higher junction temperature
|
||||
assert (
|
||||
state2.junction_temperature > state1.junction_temperature
|
||||
)
|
||||
|
||||
|
||||
class TestTemperatureDependentElectrical:
|
||||
"""Tests for temperature-dependent electrical behaviour."""
|
||||
|
||||
def test_output_voltage_varies_with_temperature(self) -> None:
|
||||
"""Test output voltage changes with junction temperature."""
|
||||
# Create custom LDO with higher tempco for observable effect
|
||||
params = LDOParameters(
|
||||
nominal_output_voltage=3.3,
|
||||
tempco_ppm_per_c=100.0, # 100 ppm/°C for visible effect
|
||||
)
|
||||
ldo = LDOModel(params=params)
|
||||
|
||||
# Test at different temperatures
|
||||
vout_25 = ldo.calculate_output_voltage(25.0)
|
||||
vout_85 = ldo.calculate_output_voltage(85.0)
|
||||
|
||||
# At 85°C with 100 ppm/°C:
|
||||
# ΔV = 3.3 * 100e-6 * 60 = 19.8mV
|
||||
delta_v = vout_85 - vout_25
|
||||
expected_delta = 3.3 * 100e-6 * 60
|
||||
|
||||
assert delta_v == pytest.approx(expected_delta, rel=0.01)
|
||||
|
||||
def test_quiescent_current_varies_with_temperature(self) -> None:
|
||||
"""Test quiescent current changes with junction temperature."""
|
||||
ldo = LDOModel()
|
||||
|
||||
# Test at different temperatures
|
||||
iq_25 = ldo.calculate_quiescent_current(25.0)
|
||||
iq_85 = ldo.calculate_quiescent_current(85.0)
|
||||
|
||||
# Default tempco is 0.003/°C (0.3%/°C)
|
||||
# At 85°C: Iq = Iq_25 * (1 + 0.003 * 60) = Iq_25 * 1.18
|
||||
expected_ratio = 1.0 + 0.003 * 60
|
||||
|
||||
assert iq_85 / iq_25 == pytest.approx(expected_ratio, rel=0.01)
|
||||
|
||||
def test_power_dissipation_calculation(self) -> None:
|
||||
"""Test power dissipation is calculated correctly."""
|
||||
ldo = LDOModel()
|
||||
ldo.input_voltage = 5.0
|
||||
|
||||
# P = (Vin - Vout) * Iload + Vin * Iq
|
||||
# At 25°C: Vout ≈ 3.3V, Iq ≈ 50µA
|
||||
# With 100mA load: P ≈ (5-3.3)*0.1 + 5*50e-6 ≈ 0.170W
|
||||
p_diss = ldo.calculate_power_dissipation(
|
||||
input_voltage=5.0,
|
||||
load_current=0.1,
|
||||
junction_temperature=25.0,
|
||||
)
|
||||
|
||||
expected_p = (5.0 - 3.3) * 0.1 + 5.0 * 50e-6
|
||||
assert p_diss == pytest.approx(expected_p, rel=0.01)
|
||||
|
||||
|
||||
class TestPhysicsEngineCoupling:
|
||||
"""Tests for complete thermal-electrical coupling in the engine."""
|
||||
|
||||
def test_thermal_electrical_feedback(self) -> None:
|
||||
"""Test that thermal and electrical states are coupled.
|
||||
|
||||
Higher junction temperature affects Vout, which affects power
|
||||
dissipation, which affects junction temperature - a feedback loop.
|
||||
"""
|
||||
engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
# Apply power at hot chamber
|
||||
engine.set_chamber_setpoint(85.0)
|
||||
engine.set_input_voltage(5.0)
|
||||
engine.set_load_current(0.2)
|
||||
engine.set_output_enabled(True)
|
||||
|
||||
# Let settle completely
|
||||
for _ in range(100000): # 1000 seconds
|
||||
engine.step()
|
||||
|
||||
thermal = engine.get_thermal_state()
|
||||
electrical = engine.get_electrical_state()
|
||||
|
||||
# Verify the coupling:
|
||||
# 1. Chamber should be near 85°C
|
||||
assert thermal.chamber_temperature == pytest.approx(85.0, abs=0.5)
|
||||
|
||||
# 2. Case should be higher than chamber due to self-heating
|
||||
assert thermal.case_temperature > thermal.chamber_temperature
|
||||
|
||||
# 3. Junction should be higher than case
|
||||
assert thermal.junction_temperature > thermal.case_temperature
|
||||
|
||||
# 4. Output voltage should reflect temperature-adjusted value
|
||||
assert electrical.output_voltage > 0
|
||||
assert electrical.output_voltage < 5.0 # Less than input
|
||||
|
||||
# 5. Power should be non-zero
|
||||
assert electrical.power_dissipation > 0
|
||||
|
||||
def test_engine_with_custom_dut_model(self) -> None:
|
||||
"""Test engine works with custom DUT model parameters."""
|
||||
params = LDOParameters(
|
||||
nominal_output_voltage=1.8, # Different voltage
|
||||
tempco_ppm_per_c=25.0,
|
||||
)
|
||||
ldo = LDOModel(params=params)
|
||||
engine = PhysicsEngine(update_rate_hz=100.0, dut_model=ldo)
|
||||
|
||||
engine.set_input_voltage(3.3)
|
||||
engine.set_load_current(0.1)
|
||||
engine.set_output_enabled(True)
|
||||
|
||||
for _ in range(1000):
|
||||
engine.step()
|
||||
|
||||
electrical = engine.get_electrical_state()
|
||||
|
||||
# Should output approximately 1.8V
|
||||
assert electrical.output_voltage == pytest.approx(1.8, abs=0.01)
|
||||
|
||||
def test_simulation_time_accuracy(self) -> None:
|
||||
"""Test simulation time accumulates correctly."""
|
||||
engine = PhysicsEngine(update_rate_hz=1000.0) # 1ms timestep
|
||||
|
||||
for _ in range(1000):
|
||||
engine.step()
|
||||
|
||||
# Should be exactly 1 second
|
||||
assert engine.simulation_time == pytest.approx(1.0, abs=1e-6)
|
||||
|
||||
def test_multiple_setpoint_changes(self) -> None:
|
||||
"""Test engine handles multiple setpoint changes correctly."""
|
||||
engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
# Start at 25°C, go to 85°C
|
||||
engine.set_chamber_setpoint(85.0)
|
||||
for _ in range(10000): # 100 seconds
|
||||
engine.step()
|
||||
|
||||
hot_state = engine.get_thermal_state()
|
||||
assert hot_state.chamber_temperature > 75.0
|
||||
|
||||
# Now cool down to -40°C
|
||||
engine.set_chamber_setpoint(-40.0)
|
||||
for _ in range(20000): # 200 seconds
|
||||
engine.step()
|
||||
|
||||
cold_state = engine.get_thermal_state()
|
||||
assert cold_state.chamber_temperature < -30.0
|
||||
214
tests/unit/test_physics_models.py
Normal file
214
tests/unit/test_physics_models.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Unit tests for physics state dataclasses and engine stub."""
|
||||
|
||||
import pytest
|
||||
|
||||
from py_dvt_ate.simulation.physics.engine import PhysicsEngine
|
||||
from py_dvt_ate.simulation.physics.state import ElectricalState, ThermalState
|
||||
|
||||
|
||||
class TestThermalState:
|
||||
"""Tests for the ThermalState dataclass."""
|
||||
|
||||
def test_creation(self) -> None:
|
||||
"""Test ThermalState can be created with valid values."""
|
||||
state = ThermalState(
|
||||
chamber_temperature=25.0,
|
||||
case_temperature=30.0,
|
||||
junction_temperature=35.0,
|
||||
timestamp=0.0,
|
||||
)
|
||||
|
||||
assert state.chamber_temperature == 25.0
|
||||
assert state.case_temperature == 30.0
|
||||
assert state.junction_temperature == 35.0
|
||||
assert state.timestamp == 0.0
|
||||
|
||||
def test_immutability(self) -> None:
|
||||
"""Test ThermalState is immutable (frozen dataclass)."""
|
||||
state = ThermalState(
|
||||
chamber_temperature=25.0,
|
||||
case_temperature=30.0,
|
||||
junction_temperature=35.0,
|
||||
timestamp=0.0,
|
||||
)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
state.chamber_temperature = 50.0 # type: ignore[misc]
|
||||
|
||||
def test_equality(self) -> None:
|
||||
"""Test ThermalState equality comparison."""
|
||||
state1 = ThermalState(
|
||||
chamber_temperature=25.0,
|
||||
case_temperature=30.0,
|
||||
junction_temperature=35.0,
|
||||
timestamp=0.0,
|
||||
)
|
||||
state2 = ThermalState(
|
||||
chamber_temperature=25.0,
|
||||
case_temperature=30.0,
|
||||
junction_temperature=35.0,
|
||||
timestamp=0.0,
|
||||
)
|
||||
|
||||
assert state1 == state2
|
||||
|
||||
def test_hashable(self) -> None:
|
||||
"""Test ThermalState is hashable (for use in sets/dicts)."""
|
||||
state = ThermalState(
|
||||
chamber_temperature=25.0,
|
||||
case_temperature=30.0,
|
||||
junction_temperature=35.0,
|
||||
timestamp=0.0,
|
||||
)
|
||||
|
||||
# Should not raise
|
||||
hash(state)
|
||||
{state} # Can be added to a set
|
||||
|
||||
|
||||
class TestElectricalState:
|
||||
"""Tests for the ElectricalState dataclass."""
|
||||
|
||||
def test_creation(self) -> None:
|
||||
"""Test ElectricalState can be created with valid values."""
|
||||
state = ElectricalState(
|
||||
input_voltage=5.0,
|
||||
output_voltage=3.3,
|
||||
load_current=0.1,
|
||||
quiescent_current=50e-6,
|
||||
power_dissipation=0.17,
|
||||
)
|
||||
|
||||
assert state.input_voltage == 5.0
|
||||
assert state.output_voltage == 3.3
|
||||
assert state.load_current == 0.1
|
||||
assert state.quiescent_current == 50e-6
|
||||
assert state.power_dissipation == 0.17
|
||||
|
||||
def test_immutability(self) -> None:
|
||||
"""Test ElectricalState is immutable (frozen dataclass)."""
|
||||
state = ElectricalState(
|
||||
input_voltage=5.0,
|
||||
output_voltage=3.3,
|
||||
load_current=0.1,
|
||||
quiescent_current=50e-6,
|
||||
power_dissipation=0.17,
|
||||
)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
state.output_voltage = 1.8 # type: ignore[misc]
|
||||
|
||||
def test_equality(self) -> None:
|
||||
"""Test ElectricalState equality comparison."""
|
||||
state1 = ElectricalState(
|
||||
input_voltage=5.0,
|
||||
output_voltage=3.3,
|
||||
load_current=0.1,
|
||||
quiescent_current=50e-6,
|
||||
power_dissipation=0.17,
|
||||
)
|
||||
state2 = ElectricalState(
|
||||
input_voltage=5.0,
|
||||
output_voltage=3.3,
|
||||
load_current=0.1,
|
||||
quiescent_current=50e-6,
|
||||
power_dissipation=0.17,
|
||||
)
|
||||
|
||||
assert state1 == state2
|
||||
|
||||
|
||||
class TestPhysicsEngineStub:
|
||||
"""Tests for the PhysicsEngine stub."""
|
||||
|
||||
def test_creation_default(self) -> None:
|
||||
"""Test PhysicsEngine can be created with defaults."""
|
||||
engine = PhysicsEngine()
|
||||
|
||||
assert engine.dt == pytest.approx(0.01) # 100Hz -> 10ms
|
||||
|
||||
def test_creation_custom_rate(self) -> None:
|
||||
"""Test PhysicsEngine with custom update rate."""
|
||||
engine = PhysicsEngine(update_rate_hz=50.0)
|
||||
|
||||
assert engine.dt == pytest.approx(0.02) # 50Hz -> 20ms
|
||||
|
||||
def test_step_advances_time(self) -> None:
|
||||
"""Test step() advances simulation time."""
|
||||
engine = PhysicsEngine(update_rate_hz=100.0)
|
||||
|
||||
assert engine.simulation_time == pytest.approx(0.0)
|
||||
engine.step()
|
||||
assert engine.simulation_time == pytest.approx(0.01)
|
||||
engine.step()
|
||||
assert engine.simulation_time == pytest.approx(0.02)
|
||||
|
||||
def test_get_thermal_state(self) -> None:
|
||||
"""Test get_thermal_state returns ThermalState."""
|
||||
engine = PhysicsEngine()
|
||||
|
||||
state = engine.get_thermal_state()
|
||||
|
||||
assert isinstance(state, ThermalState)
|
||||
assert state.chamber_temperature == 25.0 # Default
|
||||
assert state.timestamp == 0.0
|
||||
|
||||
def test_get_electrical_state(self) -> None:
|
||||
"""Test get_electrical_state returns ElectricalState."""
|
||||
engine = PhysicsEngine()
|
||||
|
||||
state = engine.get_electrical_state()
|
||||
|
||||
assert isinstance(state, ElectricalState)
|
||||
|
||||
def test_set_chamber_setpoint(self) -> None:
|
||||
"""Test set_chamber_setpoint updates setpoint."""
|
||||
engine = PhysicsEngine()
|
||||
|
||||
engine.set_chamber_setpoint(85.0)
|
||||
|
||||
# Stub doesn't update chamber temp, just stores setpoint
|
||||
state = engine.get_thermal_state()
|
||||
assert state.chamber_temperature == 25.0 # Still at initial
|
||||
|
||||
def test_set_input_voltage(self) -> None:
|
||||
"""Test set_input_voltage updates voltage."""
|
||||
engine = PhysicsEngine()
|
||||
|
||||
engine.set_input_voltage(5.0)
|
||||
|
||||
state = engine.get_electrical_state()
|
||||
assert state.input_voltage == 5.0
|
||||
|
||||
def test_set_load_current(self) -> None:
|
||||
"""Test set_load_current updates current."""
|
||||
engine = PhysicsEngine()
|
||||
engine.set_output_enabled(True)
|
||||
|
||||
engine.set_load_current(0.1)
|
||||
|
||||
state = engine.get_electrical_state()
|
||||
assert state.load_current == 0.1
|
||||
|
||||
def test_output_disabled_by_default(self) -> None:
|
||||
"""Test output is disabled by default."""
|
||||
engine = PhysicsEngine()
|
||||
|
||||
assert not engine.is_output_enabled
|
||||
|
||||
def test_enable_output(self) -> None:
|
||||
"""Test set_output_enabled enables output."""
|
||||
engine = PhysicsEngine()
|
||||
|
||||
engine.set_output_enabled(True)
|
||||
|
||||
assert engine.is_output_enabled
|
||||
|
||||
def test_load_current_zero_when_disabled(self) -> None:
|
||||
"""Test load current is zero when output disabled."""
|
||||
engine = PhysicsEngine()
|
||||
engine.set_load_current(0.1)
|
||||
|
||||
state = engine.get_electrical_state()
|
||||
|
||||
assert state.load_current == 0.0 # Disabled, so zero
|
||||
Reference in New Issue
Block a user