Entropyk/1-3-port-and-connection-system.md

336 lines
11 KiB
Markdown

# Story 1.3: Port and Connection System
Status: ready-for-dev
## Story
As a **thermodynamic systems developer**,
I want **a type-safe port and connection system with compile-time state validation**,
so that **I can prevent runtime connection errors and ensure fluid compatibility between components**.
## Acceptance Criteria
### AC 1: Port Creation
**Given** a fluid type and thermodynamic state
**When** I create a new port
**Then** it initializes in `Disconnected` state with `fluid_id`, `pressure`, and `enthalpy`
### AC 2: Fluid Compatibility Validation
**Given** two ports with different `fluid_id` values
**When** I attempt to connect them
**Then** the connection fails with `ConnectionError::IncompatibleFluid`
### AC 3: Pressure Continuity Validation
**Given** two ports with significantly different pressure values
**When** I attempt to connect them
**Then** the connection fails with `ConnectionError::PressureMismatch`
### AC 4: Enthalpy Continuity Validation
**Given** two ports with significantly different enthalpy values
**When** I attempt to connect them
**Then** the connection fails with `ConnectionError::EnthalpyMismatch`
### AC 5: Successful Connection
**Given** two compatible ports (same fluid, matching pressure/enthalpy within tolerance)
**When** I connect them
**Then** I receive two `Port<Connected>` instances with averaged thermodynamic values
### AC 6: Connected Port Operations
**Given** a `Port<Connected>` instance
**When** I access its properties
**Then** I can read `pressure()`, `enthalpy()`, and modify them via `set_pressure()`, `set_enthalpy()`
### AC 7: Compile-Time Type Safety
**Given** a `Port<Disconnected>` instance
**When** code attempts to call `pressure()` or `set_pressure()` on it
**Then** the compilation fails (Type-State pattern enforcement)
### AC 8: Component Trait Integration
**Given** the `Component` trait
**When** I implement it for a component
**Then** I can provide `get_ports()` to expose the component's connection points
## Tasks / Subtasks
- [x] **Task 1: Define Type-State Pattern Foundation** (AC: 1, 7)
- [x] Create `Disconnected` and `Connected` marker structs
- [x] Define generic `Port<State>` struct with `PhantomData`
- [x] Implement zero-cost state tracking
- [x] **Task 2: Implement Port Creation** (AC: 1)
- [x] Create `Port::new()` constructor for `Port<Disconnected>`
- [x] Store `fluid_id: FluidId`, `pressure: Pressure`, `enthalpy: Enthalpy`
- [x] **Task 3: Define Connection Errors** (AC: 2, 3, 4)
- [x] Create `ConnectionError` enum with `thiserror`
- [x] Add `IncompatibleFluid { from, to }` variant
- [x] Add `PressureMismatch { from_pressure, to_pressure }` variant
- [x] Add `EnthalpyMismatch { from_enthalpy, to_enthalpy }` variant
- [x] **Task 4: Implement Connection Logic** (AC: 2, 3, 4, 5)
- [x] Add `Port<Disconnected>::connect()` method
- [x] Validate fluid compatibility
- [x] Validate pressure continuity (tolerance: 1e-6 Pa)
- [x] Validate enthalpy continuity (tolerance: 1e-6 J/kg)
- [x] Return averaged values on success
- [x] **Task 5: Implement Connected Port Operations** (AC: 6)
- [x] Add `pressure()` getter to `Port<Connected>`
- [x] Add `enthalpy()` getter to `Port<Connected>`
- [x] Add `set_pressure()` setter to `Port<Connected>`
- [x] Add `set_enthalpy()` setter to `Port<Connected>`
- [x] Add `fluid_id()` accessor (available in both states)
- [x] **Task 6: Extend Component Trait** (AC: 8)
- [x] Add `get_ports()` method to `Component` trait
- [x] Return appropriate port references for each component type
- [x] **Task 7: Write Tests**
- [x] Test port creation with valid parameters
- [x] Test connection with compatible ports
- [x] Test error handling for incompatible fluids
- [x] Test error handling for pressure mismatches
- [x] Test error handling for enthalpy mismatches
- [x] Test value modification on connected ports
- [x] Add compile-time safety verification test
## Dev Notes
### Type-State Pattern Implementation
The Type-State pattern uses Rust's type system to encode state machines at compile time:
```rust
// Zero-cost marker types
pub struct Disconnected;
pub struct Connected;
// Generic Port with state parameter
pub struct Port<State> {
fluid_id: FluidId,
pressure: Pressure,
enthalpy: Enthalpy,
_state: PhantomData<State>, // Zero-cost at runtime
}
// Only Disconnected ports can be connected
impl Port<Disconnected> {
pub fn connect(self, other: Port<Disconnected>)
-> Result<(Port<Connected>, Port<Connected>), ConnectionError> {
// Validation logic...
}
}
// Only Connected ports expose mutable operations
impl Port<Connected> {
pub fn pressure(&self) -> Pressure { self.pressure }
pub fn set_pressure(&mut self, pressure: Pressure) { self.pressure = pressure }
}
```
### Connection Validation Logic
```rust
pub fn connect(self, other: Port<Disconnected>)
-> Result<(Port<Connected>, Port<Connected>), ConnectionError>
{
// 1. Fluid compatibility check
if self.fluid_id != other.fluid_id {
return Err(ConnectionError::IncompatibleFluid {
from: self.fluid_id.to_string(),
to: other.fluid_id.to_string()
});
}
// 2. Pressure continuity (1e-6 Pa tolerance)
let pressure_diff = (self.pressure.to_pascals() - other.pressure.to_pascals()).abs();
if pressure_diff > 1e-6 {
return Err(ConnectionError::PressureMismatch {
from_pressure: self.pressure.to_pascals(),
to_pressure: other.pressure.to_pascals()
});
}
// 3. Enthalpy continuity (1e-6 J/kg tolerance)
let enthalpy_diff = (self.enthalpy.to_joules_per_kg() - other.enthalpy.to_joules_per_kg()).abs();
if enthalpy_diff > 1e-6 {
return Err(ConnectionError::EnthalpyMismatch {
from_enthalpy: self.enthalpy.to_joules_per_kg(),
to_enthalpy: other.enthalpy.to_joules_per_kg()
});
}
// 4. Create connected ports with averaged values
let avg_pressure = Pressure::from_pascals(
(self.pressure.to_pascals() + other.pressure.to_pascals()) / 2.0
);
let avg_enthalpy = Enthalpy::from_joules_per_kg(
(self.enthalpy.to_joules_per_kg() + other.enthalpy.to_joules_per_kg()) / 2.0
);
Ok((
Port {
fluid_id: self.fluid_id,
pressure: avg_pressure,
enthalpy: avg_enthalpy,
_state: PhantomData,
},
Port {
fluid_id: other.fluid_id,
pressure: avg_pressure,
enthalpy: avg_enthalpy,
_state: PhantomData,
}
))
}
```
### Testing Approach
Use the `approx` crate for floating-point assertions:
```rust
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn test_port_creation() {
let port = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(400_000.0)
);
assert_eq!(port.fluid_id().to_string(), "R134a");
}
#[test]
fn test_incompatible_fluid_error() {
let port1 = Port::new(FluidId::new("R134a"), p1, h1);
let port2 = Port::new(FluidId::new("Water"), p1, h1);
match port1.connect(port2) {
Err(ConnectionError::IncompatibleFluid { .. }) => (), // Expected
_ => panic!("Expected IncompatibleFluid error"),
}
}
}
```
### Compile-Time Safety Verification
Create a test that intentionally fails to compile:
```rust
#[test]
fn test_compile_time_safety() {
let port: Port<Disconnected> = Port::new(
FluidId::new("R134a"),
Pressure::from_bar(1.0),
Enthalpy::from_joules_per_kg(400_000.0)
);
// This line should NOT compile - uncomment to verify:
// let _p = port.pressure(); // ERROR: no method `pressure` on Port<Disconnected>
// This should work after connection:
let port2 = Port::new(FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400_000.0));
let (mut connected, _) = port.connect(port2).unwrap();
let _p = connected.pressure(); // OK: Port<Connected> has pressure()
}
```
## Project Structure Notes
### File Locations
```
crates/
├── components/
│ ├── Cargo.toml # Add: entropyk-core dependency, approx dev-dependency
│ └── src/
│ ├── lib.rs # Add: pub mod port; extend Component trait
│ └── port.rs # NEW: Port implementation
└── core/
└── src/
└── types.rs # Pressure, Enthalpy, FluidId definitions
```
### Dependencies to Add
**In `crates/components/Cargo.toml`:**
```toml
[dependencies]
entropyk-core = { path = "../core" }
thiserror = "1.0"
[dev-dependencies]
approx = "0.5"
```
### Alignment with Project Structure
- **Path convention**: All new code in `crates/components/src/`
- **Naming**: Module name `port.rs` matches functionality
- **Trait extension**: `Component` trait extended with `get_ports()` method
- **Error handling**: Uses `thiserror` crate (standard in Rust ecosystem)
- **Testing**: Uses `approx` for floating-point comparisons (recommended for thermodynamic calculations)
## References
### Technical Documentation
- [Source: README_STORY_1_3.md] - Original implementation documentation
- [Source: demo/README.md] - Feature status tracking (Story 1.3: Ports & Connexions)
- [Source: docs/TUTORIAL.md#5] - Port system usage in tutorial
### External Resources
- [Rust Type-State Pattern](https://rust-unofficial.github.io/patterns/patterns/behavioural/phantom-types.html) - Official Rust patterns documentation
- [thiserror Documentation](https://docs.rs/thiserror/) - Error handling derive macro
- [approx Documentation](https://docs.rs/approx/) - Floating-point assertion macros
### Related Stories
- Story 1.1: Component Trait (foundation trait extended with get_ports)
- Story 1.2: Physical Types (Pressure, Enthalpy, Temperature)
- Story 1.4: Compressor (first component using port system)
## Dev Agent Record
### Agent Model Used
N/A - Story documentation created retroactively for existing implementation.
### Debug Log References
N/A - Implementation completed prior to story documentation.
### Completion Notes List
1. ✅ Type-State pattern successfully prevents runtime port state errors
2. ✅ All validation rules (fluid, pressure, enthalpy) implemented
3. ✅ Tests cover success and error cases
4. ✅ Component trait extended with `get_ports()` method
5. ✅ Documentation includes compile-time safety verification example
### File List
**Created/Modified:**
- `crates/components/src/port.rs` - Port implementation with Type-State pattern
- `crates/components/src/lib.rs` - Extended Component trait with get_ports()
- `crates/components/Cargo.toml` - Added entropyk-core dependency, thiserror, approx
**Dependencies:**
- `entropyk-core` - Physical types (Pressure, Enthalpy, FluidId)
- `thiserror` - Error derive macro
- `approx` - Floating-point test assertions (dev dependency)
---
**Story Completion Status**: ready-for-dev
**Ultimate context engine analysis completed** - comprehensive developer guide created
**Date**: 2026-02-22
**Created by**: create-story workflow (BMAD v6.0.1)