336 lines
11 KiB
Markdown
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)
|